Esfinge Comparasion - Comparação de Instâncias


Utilizando a Comparação de Instâncias


O Esfinge Comparison é um framework que faz a comparação entre duas instâncias da mesma classe e retorna uma lista de diferença entre elas. Ele utiliza introspecção para percorrer os atributos da classe e comparar os valores de cada uma das instâncias. Esse framework pode ser utilizado para recuperar a diferença entre duas versões da mesma entidade para questões de registro de auditoria (logging), para ressaltar as mudanças em um formulário, dentre outros possíveis usos…

Configurando a Comparação de Instâncias

O algoritmo de comparação a ser utilizado pode ser configurado através das anotações nas classes. Essas anotações devem ser colocadas nos métodos getters das respectivas propriedades. A seguir está uma lista de anotações disponibilizadas pelo framework para configurarem a comparação para cada classe:
  • @IgnoreInComparison : ignora o atributo anotado, não incluindo-o na comparação. Deve ser utilizado para campos que normalmente não são persistidos ou não são importantes relacionados ao negócio.
  • @DeepComparison : executa o algoritmo de comparação de forma recursiva para a propriedade. Devem ser utilizados para propriedades complexas, que possuem propriedades dentro delas. Exemplo: caso a classe Pessoa possua uma instância da classe Endereco com essa propriedade, o algoritmo irá entrar compara cada propriedade dos endereços.
  • @Tolerance: configura a tolerância numérica permitida para o campo. Deve ser usado em campos do tipo ponto flutuante onde podem haver diferenças de arredondamento.
  • @CompareSubstring: compara apenas parte da string iniciada no atributo begin e terminada no atributo end da anotação. Deve ser usado em propriedades onde apenas parte da informação é relevante na comparação.
A listagem abaixo mostra um exemplo de como essas anotação devem ser configuradas em uma classe da aplicação. Através delas é possível configurar o comportamento do algoritmo de comparação utilizado pelo componente.
public class Person{
    private String name;
    private double weight;
    private int age;
    private Address address;

    @DeepComparison
    public Address getAddress() {
        return address;
    }
    public void setAddress(Address adddress) {
        this.address = adddress;
    }
    @CompareSubstring(begin=3)
    public String getName() {
        return name;
    }
    public void setName(String nome) {
        this.name = nome;
    }
    @Tolerance(0.1)
    public double getWeight() {
        return weight;
    }
    public void setWeight(double weight) {
        this.weight = weight;
    }
    @IgnoreInComparison
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

Invocando a Comparação

Para utilizar a classe principal, chamada ComparisonComponent, basta invocar o método compare() passando as instâncias que se deseja comparar. O primeiro objeto passado como parâmetro é considerado a versão antiga e o segundo a versão nova. Abaixo segue um código que invoca o componente de comparação e imprime as diferenças retornadas por ele.
public class Test {

    public static void main(String[] args) throws CompareException {
        Person p1 = new Person("Sr. Zé",70.5f,20);
        Address e1 = new Address("Pariquis","50");
        p1.setAddress(e1);
        Person p2 = new Person("Dr. Zé",70.7f,21);
        Address e2 = new Address("Pariquis","55");
        p2.setAddress(e2);
        ComparisonComponent c = new ComparisonComponent();
        List difs = c.compare(p2, p1);

        for(Difference d : difs){
            System.out.println(d);
        }

    }
}

No caso, o retorno desse código será:

weight:70.69999694824219/70.5
address.number:55/50

Relacionamentos Circulares

Muitas vezes cria-se relacionamentos bidirecionais em objetos de domínio. Caso essas situações não sejam tratadas pelo componente de comparação, poderia-se criar um loop infinito em que o algoritmo faria a comparação dos mesmos objetos até estourar a pilha de execução. Para evitar isso, o componente de comparação armazena dentro de uma pilha os componentes que estão naquele ramo da árvore de comparação. Caso algum dos objetos já estiver nesse ramo de comparação, ele será ignorado e o algoritmo irá prosseguir sua execução. Caso o objeto esteja em um ramo diferente, como por exemplo, quando a mesma instância é setada em duas propriedades do mesmo objeto, a comparação ocorrerá normalmente.
Justamente por haver esse armazenamento de estado para a detecção de relacionamentos bidirecionais, a mesma instância do componente de comparação não deve ser compartilhada para comparações que possam acontecer em paralelo. Nesse caso, uma instância do componente deverá ser criada para cada comparação.

Configurações do Esfinge Comparison


Este tutorial apresenta configurações gerais do Esfinge Comparison que afetam o comportamento de qualquer comparação realizada. Esse tipo de configuração difere das anotações, que influenciam a comparação somente da classe anotada.

Camadas de Comparação

O componente de comparação é composto por diferentes camadas de comparação, as quais possuem diferentes responsabilidades na comparação de propriedades. As implementações dessas camadas que devem ser usadas, podem ser passadas como parâmetro para a classe ComparisonComponent em sua criação. Caso o construtor sem parâmetros seja utilizado, serão configuradas as seguintes camadas de comparação:
  • NullComparisonLayer: realiza a comparação quando pelo menos um dos valores a serem comparados é nulo.
  • DeepComparisonLayer: realiza a comparação quando deve ser feita uma comparação profunda entre os valores da propriedade.
  • CollectionItensComparisonLayer: realiza a comparação quando a propriedade é uma coleção de objetos simples ou complexos.
  • ValueComparisonLayer: realiza a comparação simples de valores, utilizando o ComparisonProcessor configurado para aquela propriedade se for o caso (será mostrado mais a frente como fazer essa configuração)
Caso deseje-se estender o componente para criar uma nova camada de comparação, deve-se criar uma classe que estenda a classe abstrata ComparisonLayer, apresentada abaixo. O método compare() é chamado pelo algoritmo de comparação para cada propriedade a ser comparada.
public abstract class ComparisonLayer {

    private ComparisonComponent component;

    public abstract boolean compare(Object oldValue, Object newValue, List difs,
                                        PropertyDescriptor descProp) throws CompareException ;
   public ComparisonComponent getComponent() {
        return component;
    }
    public void setComponent(ComparisonComponent component) {
        this.component = component;
    }
}
Como pode ser visto, o método retorna um valor booleano, o qual significa se a camada realizou ou não a comparação. Sendo assim, caso a comparação tenha sido realizada pela camada o valor retornado será true e caso contrário será false. Por exemplo, caso a propriedade passada não seja uma lista, a classe CollectionItensComparisonLayer retornará false pois não fará a comparação. O algoritmo de comparação irá invocar as camadas na ordem que forem passadas ao componente e irá parar quando uma delas retornar true, significando que a comparação já foi realizada por ela.
O método compare() recebe como parâmetro o PropertyDescriptor com as informações sobre a comparação da propriedade, assim como os valores dessa propriedade dos dois objetos a serem comparados. Além disso, ainda é passada para esse método a lista de diferenças, a qual deve ser populada caso alguma diferença seja encontrada na comparação daquela propriedade.

Leitores de Metadados

Outro ponto que pode ser estendido no framework é a leitura de metadados, que na verdade são as classes que obtém as informações sobre como a comparação deve ser feita por uma determinada classe. Uma nova classe de leitura de metadados deve implementar a interface ComparisonMetadataReader e consequentemente o método populateContainer(). Esse método, conforme mostrado abaixo na definição da interface, recebe como parâmetro a classe que está sendo processada e a instância de ComparisonDescriptor, que deve ser populada com as informações na execução desse método.
public interface ComparisonMetadataReader {
    public abstract void populateContainer(Class c, ComparisonDescriptor descriptor);
}
Os leitores de metadados podem ser configurados através do método estático set() da classe MetadataReaderProvider. A classe ChainComparisonMetadataReader pode ser usada para coordenar a leitura de metadados utilizando mais de um leitor ao mesmo tempo. O último leitor passado como parâmetro executará por último e consequentemente os metadados da fonte lida por ele terão maior prioridade.
O leitor configurado por definição é o que utiliza apenas as anotações do framework. Abaixo segue a configuração que deve ser feita para que também sejam utilizadas as anotações do JPA no processo. Obviamente outros leitores também podem ser criados para serem anexados no framework. Por exemplo, poderia-se criar um novo leitor que consideraria as anotações JAXB da classe para configurar o algoritmo de comparação.
ChainComparisonMetatataReader chainReader =
    new ChainComparisonMetatataReader(
        new AnnotationComparisonMetadataReader(),
        new JPAComparisonMetadataReader()
    );
MetadataReaderProvider.set(chainReader);

Usando os Metadados do JPA

Muitas aplicações utilizam a API JPA para a persistência, e dessa forma o Esfinge Comparison pode utilizar a algumas anotações de persistência para determinar alguns dos metadados considerados no algoritmo. Para isso o leitor de metadados JPA deve ser configurado conforme mostrado na listagem da seção anterior. Abaixo seguem as anotações do JPA que possuem algum significado especial no algoritmo de comparação:
  • @Transiente: pelo fato de uma propriedade transiente não ser persistida, uma mudança nela não representa uma mudança persistente da entidade em questão. Por esse motivo, toda propriedade com a anotação @Transient será ignorada na comparação.
  • @Entity: essa anotação em uma classe a configura como uma classe persistente. Para o componente de comparação, uma propriedade com um tipo que possua essa anotação, representa uma propriedade composta, a qual o algoritmo de comparação deve ser executado de forma recursiva. Em outras palavras, uma propriedade com um tipo anotado com @Entity é o mesmo que possuir a anotação @DeepComparison.
  • @Id e @EmbededId: essas anotações são utilizadas para identificar a propriedade responsável pela identidade da entidade. É utilizada na comparação de listas, para identificar quais são os elementos equivalentes nas duas listas no momento da comparação.

Criando Novas Anotações de Comparação

Além das anotações descritas, também é possível estender o componente de comparação através da adição de novas anotações. Essas anotações podem definir critérios de comparação diferentes para certas propriedades. A listagem a seguir mostra a anotação @CompareSubstring como exemplo. Ao criar uma nova anotação, deve-se anota-la com a anotação @DelegateReader, que recebe como parâmetro a classe responsável por ler e interpretar a anotação.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DelegateReader(SubstringComparisonReader.class)
public @interface CompareSubstring {
    int begin() default 0;
    int end() default Integer.MAX_VALUE;
}
A classe passada como parâmetro para anotação @DelegateReader precisa implementar a interface AnnotationReader com o parâmetro genérico igual a anotação a ser processada. No exemplo apresentado a seguir, por exemplo, a classe implementa AnnotationReader já que a anotação que será processada será @CompareSubstring.
public class SubstringComparisonReader implements AnnotationReader {
    @Override
    public void readAnnotation(CompareSubstring annotation, PropertyDescriptor descriptor) {
        int begin = annotation.begin();
        int end = annotation.end();
        SubstringProcessor p = new SubstringProcessor(begin,end);
        descriptor.setProcessor(p);
    }
}
A classe que implementa AnnotationReader precisa implementar o método readAnnotation() o qual recebe como parâmetro a anotação a ser lida e a instância de PropertyDescriptor relativa a descrição de como aquela propriedade deve ser comparada. A propriedade processor da classe PropertyDescriptor pode ser utilizado para armazenar uma instância que contém o algoritmo de comparação. No exemplo, uma instância da classe SubstringProcessor é criada e inserida no PropertyDescriptor através do método setProcessor().
As classes que processam a comparação precisam implementar a interface ComparisonProcessor, como a classe SubstringProcessor representada abaixo. Essa classe pode possuir propriedades, normalmente populadas com informações das anotações. Ela precisa implementar o método compare() que será utilizado no algoritmo de comparação para comparar as propriedades anotada com a anotação criada.
public class SubstringProcessor implements ComparisonProcessor {

    private int begin;
    private int end;

    public SubstringProcessor(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }
    @Override
    public Difference compare(String prop, Object oldValue, Object newValue) {
        if (newValue == null) {
            if (oldValue != null) {
                return new PropertyDifference(prop, newValue, oldValue);
            }
        } else{
            String oldString, newString;
            if(end == Integer.MAX_VALUE){
                oldString = oldValue.toString().substring(begin);
                newString = newValue.toString().substring(begin);
            }else{
                oldString = oldValue.toString().substring(begin, end);
                newString = newValue.toString().substring(begin, end);
            }
            if(!oldString.equals(newString))
                return new PropertyDifference(prop, newValue, oldValue);
        }
        return null;
    }
}
O método compare() recebe como parâmetro o nome da propriedade que está comparando e os valores das duas instâncias comparadas. Caso os valores devam ser considerados iguais, esse método deve retornar null. Caso haja alguma diferença, deve ser retornada uma instância da classe abstrata Difference.
O caso mais comum é ser uma instância de PropertyDifference, que representa uma diferença entre valores de propriedades. Na próxima seção será apresentado um novo tipo de diferença na comparação de listas. Caso uma nova subclasse de Difference seja criada, ela poderia ser retornada nesse método sem problemas.
Sendo assim, para estender o componente de comparação, criando novos algoritmos para a comparação de propriedades mais específicas, é necessário criar a anotação, uma implementação de AnnotationReader para ler essa anotação e criar o processador e uma implementação de ComparisonProcessor que faz efetivamente a comparação customizada da propriedade.

Lidando com Listas


Existem dois tipos de comparação de listas: a comparação de listas de objetos simples e a comparação de listas de objetos compostos. Na comparação de listas com objetos simples, apenas a inclusão e exclusão de elementos são consideradas. Na comparação de objetos compostos, as propriedades de objetos que existam nas duas listas também são comparadas.

O Funcionamento da Comparação de Listas

Caso o objeto possua uma propriedade que possa ser atribuída ao tipo Collection, a mesma será tratada como uma comparação de listas. Se não houver algum metadado indicando o contrário (será apresentado mais a frente), a comparação será feita como listas simples. Nesse caso, os itens da lista são comparados usando seu método equals(). Os itens que constarem na lista antiga e não estiverem mais na lista nova serão considerados exclusões e os que estiverem na lista nova e não constam na antiga são registrados como adições.
Esse tipo de diferença gera a adição de uma instância de ListChangeDifference, que estende a classe Difference, na lista de diferenças que será retornada pelo componente. Essa classe possui a propriedade item que é populada com o item que foi adicionado ou removido e a propriedade changeType, que possui como tipo o enum ListChange. Essa propriedade adquire o valor ADDED ou REMOVED se o item foi respectivamente adicionado ou removido.
A comparação da lista será configurada para lista de objetos complexos em duas condições: (a) a coleção possuir como tipo genérico uma classe anotada com @Entity; ou (b) a propriedade estiver anotada com a anotação @DeepComparison. Nesse caso, além da verificação em relação a adição e remoção de itens, as propriedades de cada item também serão comparadas.

Na comparação de coleções com objetos complexos, o método equals() não é mais utilizado para identificar itens que representam a mesma entidade. Nesse caso, é procurada na classe da entidade uma propriedade anotada com @Id ou @EmbededId que será utilizada para identificação da entidade.

Caso na lista nova seja encontrado algum objeto com o identificador nulo, aquele item será sempre tratado como uma adição. Outros casos de adição e remoção são tratados de forma equivalente a comparação de listas de objetos simples. No caso de entidades com mesmo valor para a propriedade identificadora serem identificadas nas duas listas, ambas são submetidas ao algoritmo de comparação para que se procure diferenças em suas propriedades. Essa comparação segue as mesmas regras de uma comparação normal.

Caminhos das Propriedades em Listas

Caso seja encontrada alguma diferença na comparação de duas instâncias de uma classe, deverá ser criada uma instância da classe Difference que será retornada na lista do método de comparação. Essa classe que representa a diferença possui uma propriedade chamada “path” que armazena o caminho de onde foi encontrada a diferença em relação ao objeto-raiz da comparação. O objeto raiz é o objeto passado pela aplicação para o método de comparação.
O caso mais simples é quando a diferença está em uma propriedade do próprio objeto-raiz. Nesse caso, o nome do caminho é o próprio nome da propriedade. Por exemplo, se a diferença for na propriedade “nome”, o path armazenará o valor “nome”. Vale ressaltar que o objeto-raiz é a referência principal, porém nenhum nome entra no caminho representando-o.
Outro caso seria da diferença ser em propriedades compostas, ou seja, ser em uma propriedade que fica dentro de um objeto que é o valor de uma propriedade do objeto-raiz. Um exemplo seria de uma propriedade “rua” dentro de uma propriedade “endereco”. Nesse caso, o caminho entre as propriedades será mapeada por um ponto. Nesse caso, o valor do caminho seria “endereco.rua”. Isso pode se estender enquanto o componente de comparação for encontrando a comapração profunda configurada. Sendo assim, seriam possíveis três níveis, como “contato.telefone.ddd”.
No caso das diferenças de adição e remoção de elementos na comparação de listas, o caminho é configurado da mesma forma. A propriedade a que o caminho irá se referir será a propriedade que contém a lista.
No caso da lista, o caminho terá um formato diferente caso a diferença seja em uma propriedade de um dos itens da lista. Nesse caso, o valor do identificador do item da lista será utilizado entre colchetes para identifica-lo. Imagine que exista uma propriedade chamada “contatos” na qual seja identificada uma diferença na propriedade “nome” do elemento com identificador igual a 5. O caminho da diferença ficará “contatos[id=5].nome”.

Criando Testes da Comparação


Em caso de classes de domínio mais complexas, o desenvolvedor pode querer testar como as propriedades dessas classes são comparadas. Nesse caso, o que estará sendo testado na verdade é a configuração de metadados da classe. Abaixo segue um exemplo para ser usado como modelo de um teste desse tipo, que verifica uma propriedade simples e uma composta.
public class TesteComaparacaoPessoa {

    @BeforeClass
    public static void configuraReader(){
        ChainComparisonMetatataReader chainReader =
            new ChainComparisonMetatataReader(
                new AnnotationComparisonMetadataReader(),
                new JPAComparisonMetadataReader()
            );
        MetadataReaderProvider.set(chainReader);
    }

    @Test
    public void testePropriedadeSimples() throws CompareException{
        Person p1 = new Person("José", 80, 23);
        Person p2 = new Person("José", 83, 23);

        ComparisonComponent cc = new ComparisonComponent();
        List list = cc.compare(p1, p2);

        assertEquals("weight", list.get(0).getPath());
        assertEquals(83.0, ((PropertyDifference)list.get(0)).getNewValue());
        assertEquals(80.0, ((PropertyDifference)list.get(0)).getOldValue());
    }

    @Test
    public void testePropriedadeComposta() throws CompareException{
        Person p1 = new Person("José", 80, 23);
        Address e1 = new Address("Pariquis","50");
        p1.setAddress(e1);
        Person p2 = new Person("José", 80, 23);
        Address e2 = new Address("Pariquis","55");
        p2.setAddress(e2);

        ComparisonComponent cc = new ComparisonComponent();
        List list = cc.compare(p1, p2);

        assertEquals("address.number", list.get(0).getPath());
        assertEquals("55", ((PropertyDifference)list.get(0)).getNewValue());
        assertEquals("50", ((PropertyDifference)list.get(0)).getOldValue());
    }

}

 

 

 

Apoio

Todos os direitos reservados