caelum / vraptor4

A web MVC action-based framework, on top of CDI, for fast and maintainable Java development.
http://vraptor.org
Apache License 2.0
350 stars 333 forks source link

GsonSerializer Erro include/exclude #933

Closed jeancrbecker closed 9 years ago

jeancrbecker commented 9 years ago

Ao retornar um json através do result, quando este objeto retornado contém uma lista e tento excluir ou incluir algum parametro da mesma ocorre o erro abaixo:

java.lang.NullPointerException
    at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:191)
    at br.com.caelum.vraptor.serialization.Serializee.reflectField(Serializee.java:159)
    at br.com.caelum.vraptor.serialization.Serializee.getParentTypes(Serializee.java:140)
    at br.com.caelum.vraptor.serialization.Serializee.getParentTypesFor(Serializee.java:125)
    at br.com.caelum.vraptor.serialization.Serializee.excludeAll(Serializee.java:96)
    at br.com.caelum.vraptor.serialization.gson.GsonSerializer.exclude(GsonSerializer.java:59)

Um exemplo de objetos.

public class Resultado {
    private String nome;
    private Collection list;
}

public class Usuario {
    private String nome;
    private String login;
}

Então na controller quando tento retornar um json excluindo algum parametro da lista, ou tentando incluir apenas alguns parametros ocorre erro.

Isso não funciona

    @Get("search/{nome}")
    public void pesquisar(String nome) {
        result.use(json()).withoutRoot().from(abstractService.search(nome)).include("list").exclude("list.login").serialize();
    }

Isso também não

@Get("search/{nome}")
    public void pesquisar(String nome) {
        result.use(json()).withoutRoot().from(abstractService.search(nome)).include("list.nome").serialize();
    }
lucascs commented 9 years ago

mas esse search está retornando null? Se não, vc não precisa colocar esse "list" na frente dos atributos, ele vai aplicar nos elementos da lista.

jeancrbecker commented 9 years ago

O search funciona sem problemas, só estou com dificuldade com o include e exclude que não funcionam para os atributos dos objetos da lista. Quando uso .recursive() traz tudo, porem quero limitar o resultado para apenas algumas propriedades dos objetos da lista. Em 29/01/2015 23:16, "Lucas Cavalcanti" notifications@github.com escreveu:

mas esse search está retornando null? Se não, vc não precisa colocar esse "list" na frente dos atributos, ele vai aplicar nos elementos da lista.

— Reply to this email directly or view it on GitHub https://github.com/caelum/vraptor4/issues/933#issuecomment-72137518.

lucascs commented 9 years ago

Tentou tirar o "list."?

jeancrbecker commented 9 years ago

Sim também da erro, veja que no .from() tenho um objeto, além de alguns parâmetros ele também tem uma lista, então desta lista é que preciso enviar apenas alguns parâmetros e não todos, por isso estou tentando usar o include e o exclude mas sem sucesso.

Em 30/01/2015 01:27, "Lucas Cavalcanti" notifications@github.com escreveu:

Tentou tirar o "list."?

— Reply to this email directly or view it on GitHub https://github.com/caelum/vraptor4/issues/933#issuecomment-72148508.

jeancrbecker commented 9 years ago

Lucas, vi que tem outra issue aberta na versão 3 do VRaptor, o problema é o mesmo que estou enfrentando. https://github.com/caelum/vraptor/issues/438

jeancrbecker commented 9 years ago

Tem algum paliativo que eu possa usar @Turini @lucascs ?

Turini commented 9 years ago

Opa @jeancrbecker, dei uma olhada agora. Com o mesmo cenário que você, fiz um include em "list" e depois um exclude em "list.nome" e funcionou sem nenhum problema. Não consegui reproduzir seu bug. Você consegue escrever um projeto simples reproduzindo o bug e subir no github ou dropbox da vida?

Turini commented 9 years ago

Ahhh, esqueci de perguntar. Qual a versão do VRaptor que você está usando?

jeancrbecker commented 9 years ago

@Turini mas você serializou a partir de um objeto "container" ? Veja que quando eu serializo uma lista no .from() funciona sem problemas o acesso à suas propriedades, acontece o erro quando eu tenho um objeto, digamos pessoa, e esse objeto pessoa tem uma lista de telefones por exemplo, quando tento fazer List pessoas = servico.consulta(); ...from(pessoas).include("telefones.tipoTelefone")... então não funciona. Estou usando a versão 4.1.0

Turini commented 9 years ago

eu escrevi o código que você postou aqui na issue:

result.use(json()).withoutRoot()
    .from(abstractService.search(nome))
    .include("list").exclude("list.login").serialize();

com as mesmas classes que você postou aqui tb

jeancrbecker commented 9 years ago

Pode me enviar ou me passar um link de um projeto de testes padrão vraptor ? Ai posso usar como base para escrever o teste com meu problema. Acho que fica mais fácil para eu te passar.

Turini commented 9 years ago

essa sua Collection<Usuario> list vem do hibernate? se sim, será que é por ser um proxy que ainda não foi inicializado? tenta chamar um getList().size() antes de mandar serializar pra testar?

Turini commented 9 years ago

o link do projeto de testes que pediu: https://github.com/caelum/vraptor4/tree/master/vraptor-blank-project fica nesse mesmo repositório

jeancrbecker commented 9 years ago

@Turini https://drive.google.com/folderview?id=0BxIx81dIEEMdbTZoLU8zVzJDNzg&usp=sharing subi uma simulação do erro.

Turini commented 9 years ago

oi @jeancrbecker, baixei aqui o projeto e o problema é que você faz:

.include("telefones").include("telefones.tipo")

No primeiro include, do "telefones", ele já poe o "tipo". Tirando o segundo include o resultado vai ser o json com a lista de telefones e todos seus atributos nele. Se você mudar o teste, colocando exclude "telefone.tipo":

.include("telefones").exclude("telefones.tipo")

Vai ver que ele funciona tb. adiciona todos os campos do telefone, menos o tipo.

jeancrbecker commented 9 years ago

@Turini estranho pq aqui se eu faço

result.use(json()).withoutRoot().from(popularPessoa()).include("telefones").serialize(); 

O teste da erro e o json resultado não contém o "tipo"

Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.189 sec <<< FAILURE!
shouldNotThrowNullPointersOnJsonResultList(br.com.jbkr.jbkr.test.JsonResultTest)  Time elapsed: 0.139 sec  <<< FAILURE!
java.lang.AssertionError: 
Expected: is "[{\"nome\":\"João\",\"telefones\":[{\"numero\":\"9999-1111\",\"tipo\":{\"nome\":\"1\"}},{\"numero\":\"9999-2222\",\"tipo\":{\"nome\":\"2\"}}]},{\"nome\":\"Maria\",\"telefones\":[{\"numero\":\"9999-1111\",\"tipo\":{\"nome\":\"1\"}},{\"numero\":\"9999-2222\",\"tipo\":{\"nome\":\"2\"}}]},{\"nome\":\"Fulano\",\"telefones\":[{\"numero\":\"9999-1111\",\"tipo\":{\"nome\":\"1\"}},{\"numero\":\"9999-2222\",\"tipo\":{\"nome\":\"2\"}}]}]"
     but: was "[{\"nome\":\"João\",\"telefones\":[{\"numero\":\"9999-1111\"},{\"numero\":\"9999-2222\"}]},{\"nome\":\"Maria\",\"telefones\":[{\"numero\":\"9999-1111\"},{\"numero\":\"9999-2222\"}]},{\"nome\":\"Fulano\",\"telefones\":[{\"numero\":\"9999-1111\"},{\"numero\":\"9999-2222\"}]}]"
Turini commented 9 years ago

ah, entendi! É que faltou o .recursive() no seu exemplo. Muda pra:

result.use(json()).withoutRoot().from(popularPessoa())
    .include("telefones").recursive().serialize();

É que o tipo é uma outra classe, por padrão só o include não rola mesmo.

jeancrbecker commented 9 years ago

@Turini Esse é o problema eu não posso usar o recursive(), esse é só um exemplo básico, mas no meu projeto o objeto tem vários níveis, então vai acabar caindo em referencia circular ou mesmo onerando o tempo devido ao excesso de dados desnecessários. Por isso eu tento usar o include().

linyatis commented 9 years ago

@Turini, pelo que entendi do caso do @jeancrbecker, acho que ele pode fazer: .include("telefones", "telefones.tipo")

Assim como o @jeancrbecker, eu costumo não usar o recursive() por conta de referências circulares e objetos lazy. Eu uso o include aqui com vários níveis e funciona normalmente.

jeancrbecker commented 9 years ago

Quando tento fazer include do "tipo" ele da o erro abaixo:

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.118 sec <<< FAILURE!
shouldNotThrowNullPointersOnJsonResultList(br.com.jbkr.jbkr.test.JsonResultTest)  Time elapsed: 0.073 sec  <<< ERROR!
java.lang.IllegalArgumentException: Field path 'telefones.tipo' doesn't exists in interface java.util.List
    at br.com.caelum.vraptor.serialization.Serializee.getParentTypes(Serializee.java:145)
    at br.com.caelum.vraptor.serialization.Serializee.getParentTypesFor(Serializee.java:129)
    at br.com.caelum.vraptor.serialization.Serializee.includeAll(Serializee.java:118)
    at br.com.caelum.vraptor.serialization.gson.GsonSerializer.include(GsonSerializer.java:126)
    at br.com.jbkr.jbkr.test.JsonResultTest.shouldNotThrowNullPointersOnJsonResultList(JsonResultTest.java:42)
linyatis commented 9 years ago

Sua lista não está com o tipo de objeto definido:

public List getTelefones() {
            return telefones;
        }

Tenta com o abaixo que deve funcionar:

public List<Telefone> getTelefones() {
            return telefones;
        }
linyatis commented 9 years ago

Ah, muda também o seu atributo dentro da classe pessoa:

public class Pessoa {

        List<Telefone> telefones;
jeancrbecker commented 9 years ago

Ai que está o problema, não posso definir tipo. Eu estou fazendo um módulo genérico pra evitar o DRY, ja consegui desenvolver toda a parte de controller e serviços genéricos para CRUD, agora o objeto que vai carregar as listas repassar para o result serializar via JSON para minha app Angular não sabe qual é a tipagem da lista. Vi que quando tem a tipagem realmente eu consigo fazer o include do "telefones.tipo", será que tem alguma ideia de como posso ter o mesmo comportamento sem a tipagem ?

linyatis commented 9 years ago

Entendi, @jeancrbecker. Na verdade eu também já tive esse problema que você está comentando. Pelo que vi o include() não suporta objetos genéricos, né @Turini?

Mas também acho que seria uma feature interessante permitir objetos genéricos.

jeancrbecker commented 9 years ago

Pois é, senão vou ter que criar um container diferente para cada tipo de objeto que vier do banco para mandar pra tela, ai o DRY vai comer solto rsrs.

Turini commented 9 years ago

É sim. Agora entendi o problema. @jeancrbecker, por favor corrige a descrição da issue que diz que o .include("list").exclude("list.login")não funciona. É o include em tipo generico que não funciona, né? Ou o exclude não funciona pra você?

jeancrbecker commented 9 years ago

@Turini O exclude quando é lista generica também nao funciona.

Turini commented 9 years ago

quando você diz genérica, quer dizer sem o generics então, né? :) tipo List telefones

Turini commented 9 years ago

ou você está fazendo com List<T> ou algo assim?

jeancrbecker commented 9 years ago

Isso, sem o generics.

Turini commented 9 years ago

Então muda a descrição que mostra a lista como uma Collection<Usuario>? Se não vai confundir quem cair nesse issue depois (já que assim vai funcionar)

jeancrbecker commented 9 years ago

Ok alterado.

jeancrbecker commented 9 years ago

@Turini sabe me dizer se consigo usar algum paliativo para o meu problema ?

Turini commented 9 years ago

Opa @jeancrbecker, perdão! Ainda não consegui parar pra olhar o código disso. Estou dando uma olhada agora pra ver se conseguimos ao menos um workaround, mas adianto que só List vai ser complicado :) você não pode usar um List<T>?

jeancrbecker commented 9 years ago

@Turini Pior que não consigo senão vou ter que escrever um container para cada objeto, mas aí estaria fazendo o Ctrl+c Ctrl+v o que não acho muito legal, esse list teria que ser mesmo genérico.

asouza commented 9 years ago

Dá para usar uma annotation que nem o Hibernate usava antes, para suportar mapeamento de coleções que não usavam Generics.

@OneToMany(targetEntity = Telefone.class)

Claro que devemos criar outra annotation... Acho até uma feature justa :).

Em Thu Feb 19 2015 at 11:43:54 AM, jeancrbecker notifications@github.com escreveu:

@Turini https://github.com/Turini Pior que não consigo senão vou ter que escrever um container para cada objeto, mas aí estaria fazendo o Ctrl+c Ctrl+v o que não acho muito legal, esse list teria que ser mesmo genérico.

— Reply to this email directly or view it on GitHub https://github.com/caelum/vraptor4/issues/933#issuecomment-75053501.

rafaelGuerreiro commented 9 years ago

[Off-topic] Eu acredito que essa issue evidencia um problema de design de código. O que aconteceria caso a sua entidade Resultado sofresse uma alteração em que fosse incluído mais um atributo, que poderia ser uma lista?

Eu evito serializar as entidades, pois, dependendo da alteração, ela pode quebrar os meus JSONs. E, provavelmente, você vai ter que ficar caçando aonde você deve fazer include e aonde você deve fazer exclude.

Por isso eu crio wrappers específicos para cada tipo de JSON que eu pretendo serializar. Essa classe vai ser usada apenas para ser serializada:

@Entity
public class Company {
   private Long id;
   private String name;
   private String address;

   private List<Customer> customers;

   private List<BankAccount> accounts;
}

Se eu quiser serializar a Company para retornar apenas o id, nome e address para uma determinada view, eu crio o seguinte:

public class SomeViewCompanyWrapper {
   private final Long id;
   private final String name;
   private final String address;

   public SomeViewCompanyWrapper(Company c) {
      this.id = c.getId();
      this.name = c.getName();
      this.address = c.getAddress();
   }
}

Assim, posso serializar o objeto e, só vou alterá-lo quando eu precisar alterar a minha view.

No caso da lista, poderia ficar assim:

public class OtherViewCompanyWrapper {
   private final Long id;
   private final String name;
   private final List<OtherViewCustomerWrapper> customers;

   public SomeViewCompanyWrapper(Company c) {
      this.id = c.getId();
      this.name = c.getName();

      this.customers = toWrapper(c.getCustomers());
   }

   private List<OtherViewCustomerWrapper> toWrapper(List<Customer> customers) {
      // Implementação.
      return new ArrayList<>();
   }
}

Assim, posso usar o recursive() sem medo de que tem coisa a mais ou de que minha performance vai ser prejudicada.

jeancrbecker commented 9 years ago

@rafaelGuerreiro Esse é exatamente o caso, esse é um Wrapper especifico para um JSON meu que monta tabelas, ele é genérico justamente porque é exatamente igual para todos meus objetos básicos, muda apenas o conteúdo da lista, por isso preciso dos includes e excludes que tenho parametrizados e bem desacoplados em cada controller, sem essa correção da issue vou ter que criar o mesmo objeto para cada entidade minha do banco, imagine só que bonito o código. ResultadoPessoa.class, ResultadoEndereco.class, ResultadoBairro.class, ResultadoEtc.class... todos com os mesmos campos apenas sendo diferenciados pela tipagem da lista.

rafaelGuerreiro commented 9 years ago

Mas você não está colocando a entidade na lista? Não seria ideal fazer um wrapper genérico para essa lista? Algo assim:

private final List<ResultadoWrapperOfList> list;
public class ResultadoWrapperOfList {
   // Atributos das entidades que devem ser usados da mesma forma

   public ResultadoWrapperOfList (Object o) {
      // Carrega os atributos. pode usar algum framework de mapeamento
   }
}

Logo, você vai poder usar o recursive()...

jeancrbecker commented 9 years ago

@rafaelGuerreiro Não pois a responsabilidade de montar a tabela é da view(AngularJS), de acordo com a view ela sabe quais campos estão vindo e se ocupa de montar a tabela. Uma view pode ter a tabela com o campo Nome e outra não, então cairia no mesmo problema, teria que ter um wrapper para cada tipo de objeto que fosse serializado na lista o que não é ideal. Já tenho a arquitetura funcionando sem problemas, só esbarro no problema de que por não funcionar o include/exclude dos campos da lista quando preciso mandar um campo de relacionamento ele não vai para view, exemplo: está indo todos os dados para montar a tabala de bairro, mas não tenho na view o relacionamento com o campo Cidade para exibir na tabela.

rafaelGuerreiro commented 9 years ago

Como o @Turini disse, tente usar algum type na lista.

Você pode usar algo assim:

public class Resultado<T> {
    private List<T> lista;
}

Assim, você pode setar o tipo da lista na hora de instanciar o Resultado... Vai ficar meio esquisito:

Resultado<Customer> r = new Resultado<Customer>(company); // Usando as minhas classes mencionadas acima...

Talvez você consiga resolver se fizer isso:

public class Resultado {
    private List<? extends Object> lista;
}

Você já tentou fazer isso?

rafaelGuerreiro commented 9 years ago

@asouza Acho que essa anotação não resolveria o problema do @jeancrbecker, pois ele não tem como saber qual será o tipo da Collection. Como não conseguimos mudar o valor do parâmetro da annotation em runtime, acredito que a saída mais plausível seria usar o Generic Type mesmo...

Acho que essa anotação serveria para manter compatibilidade com códigos legados em que não fizeram o uso do Generic Type, é essa a sua ideia?

jeancrbecker commented 9 years ago

@rafaelGuerreiro tentei usar com "? extends Object" mas também não funcionou, só funciona quando coloco explicitamente List por exemplo. Com Generic Type também não rolou.

nhada commented 9 years ago

Pessoal, Tudo indica que estou aqui com o mesmo problema! Já existe uma solução?

jeancrbecker commented 9 years ago

Pois é @nhada o projeto do VRaptor parece estar meio abandonado, não tive retorno também.

felipeweb commented 9 years ago

@jeancrbecker e @nhada estamos trabalhando nisso e traremos uma solução o mais rápido o possível.

Turini commented 9 years ago

@jeancrbecker, perdão pela demora. Tirei a label de bug, porque esse é o comportamento esperado mesmo, já que a informação genérica é essencial pro include/exclude do serializador funcionar nesses casos. Pra ajudar com o seu caso de uso, mandei um pull request #961 aumentando a visibilidade de um método da classe Serializee e passando o field como parametro, para que você possa fazer algo como:

@Specializes 
public class CustomSerializee extends Serializee {
@Override
protected static Class<?> getActualType(Field field) {
        if (field.isAnnotationPresent(GenericType.class)) {
            return field.getAnnotation(GenericType.class).value();
        }
        return super.getActualType(field);
    }
}

Isso vai funcionar de forma parecida com a estratégia do hibernate, você vai criar uma annotation -- que nesse caso eu chamei de GenericType -- parecida com:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GenericType {
    Class<?> value();
}

E anotar seus atributos dessa forma:

public class Pessoa {

        @GenericType(Telefone.class) 
        private List telefones;
}

Faz sentido? Fiz essa alteração no código de testes que você me enviou e funciona perfeitamente. Essa alteração pode entrar no próximo release, que não deve demorar pra sair (se tudo correr bem, semana que vem). Se quiser, até lá posso gerar um snapshot pra você ir usando/testando. É só me avisar

jeancrbecker commented 9 years ago

Me parece perfeito Turini, se puder gerar um snap posso ir alterando meu projeto. Desde já agradeço. Em 01/04/2015 00:42, "Rodrigo Turini" notifications@github.com escreveu:

@jeancrbecker https://github.com/jeancrbecker, perdão pela demora. Tirei a label de bug, porque esse é o comportamento esperado mesmo, já que a informação genérica é essencial pro include/exclude do serializador funcionar nesses casos. Pra ajudar com o seu caso de uso, mandei um pull request #961 https://github.com/caelum/vraptor4/pull/961 aumentando a visibilidade de um método da classe Serializee e passando o field como parametro, para que você possa fazer algo como:

@Specializes public class CustomSerializee extends Serializee {@Overrideprotected static Class<?> getActualType(Field field) { if (field.isAnnotationPresent(GenericType.class)) { return field.getAnnotation(GenericType.class); } return super.getActualType(field); } }

Isso vai funcionar de forma parecida com a estratégia do hibernate, você vai criar uma annotation -- que nesse caso eu chamei de GenericType -- parecida com:

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME)public @interface GenericType { Class<?> value(); }

E anotar seus atributos dessa forma:

public class Pessoa {

    @GenericType(Telefone.class)
    private List telefones;

}

Faz sentido? Fiz essa alteração no código de testes que você me enviou e funciona perfeitamente. Essa alteração pode entrar no próximo release, que não deve demorar pra sair (se tudo correr bem, semana que vem). Se quiser, até lá posso gerar um snapshot pra você ir usando/testando. É só me avisar

— Reply to this email directly or view it on GitHub https://github.com/caelum/vraptor4/issues/933#issuecomment-88340880.

Turini commented 9 years ago

Perfeito! Acabei de deployar o 4.2.0-RC2-SNAPSHOT. Me avisa se deu certo?

jeancrbecker commented 9 years ago

@Turini estou tentando fazer a classe especializada mas não é possível fazer o Override do método, não sei se estou esquecendo alguma coisa mas simplesmente não funciona.