VRaptor 3 – Evitando CircularReferenceException do XStream

Quem utiliza a serialização JSON do VRaptor 3 em algum momento já se deparou com a
CircularReferenceException ao trabalhar com coleções. O XStream da Thoughtworks é o responsável por essa serialização e hoje iremos falar um pouco dessa biblioteca.

Objetivo:
Entender alguns conceitos do Hibernate, assim como entender e evitar o CircularReferenceException do XStream na serialização JSON de um objeto retornado que possua uma coleção.

Primeiramente vamos entender alguns conceitos:

FetchType.LAZY e FetchType.EAGER

Um objeto que possua uma coleção anotada com FetchType.LAZY não terá esta carregada junto ao objeto no qual pertence, diferentemente se anotássemos com FetchType.EAGER que já carrega a coleção junto ao seu objeto. O LAZY só carrega a coleção de fato, quando a requisitamos através de um método get.

Se eu pesquisasse o seguinte usuário no banco, eu já teria de “prima” a lista de mensagens, mas não a de filmes:

@Entity
public class Usuario {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nome;

    @OneToMany(mappedBy = "usuario", fetch = FetchType.LAZY)
    private Collection<Filme> filmeList;

    @OneToMany(mappedBy = "usuario", fetch = FetchType.EAGER)
    private Collection<Mensagem> mensagemList;

    // getters and setters

}

A propósito, não é necessário declarar que uma lista é LAZY, pois por padrão é esta sua configuração.

A lista de mensagens já é carregada logo na consulta do usuário, pois esta configurada como EAGER (Ansiosa), porém a de filme é uma lista LAZY (preguiçosa) e só será carregada quando fizermos:

    Usuario usuario = usuarioDao.loadById(1l); // Já temos a lista de mensagens carregada.
    Collection<Filme> filmeList = usuario.getFilmeList(); // Busca a lista de filmes somente neste momento.

Você pode estar se perguntando: qual era o valor dessa lista anteriormente? Como o Hibernate consegue buscar a lista a partir de um get? E ai que entra um outro conceito.

Hibernate Proxy:

O Hibernate por padrão com a ajuda da CGLIB (Code Generation Library) cria um Proxy para cada classe que você mapea. Esse Proxy é uma representação do seu objeto, contendo a mesma interface e um código para invocar a JDBC.

Quando consultamos nosso objeto Usuario o que o Hibernate nos retorna na verdade é um Proxy e não a nossa lista real. O Proxy implementa PersistentBag, logo ele sabe como invocar métodos no banco para fazer as consultas.

Não se iluda com o FetchType.EAGER, pois ele não evitará a exception. Ele unicamente carregará nosso target evitando a ida no banco posteriormente.

Agora que você já conhece um pouco mais do Hibernate, vamos nos focar na seguinte exception:

com.thoughtworks.xstream.core.TreeMarshaller$CircularReferenceException

Isso ocorre porque o Hibernate nos retorna uma PersistentBag que contém uma referência para a classe na qual ela pertence causando a referência cíclica. O XStream sabe converter apenas algumas implementações como ArrayList, LinkedList, HashSet e poucas outras, mas para o PersistBag ele serializa da forma padrão: cada campo, inclusive o da referência da instância, o que causa a referência cíclica.

Evitando a serialização do Proxy:

Para resolvermos isso precisaremos extender a classe XStreamJSONSerialization e fazer um pequeno ajuste na serialização.

@Component
public class CustomJSONSerialization extends XStreamJSONSerialization {

    public CustomJSONSerialization(HttpServletResponse response, TypeNameExtractor extractor, ProxyInitializer initializer) {
    super(response, extractor, initializer);
    }

    @Override
    protected XStream getXStream() {
        XStream xstream = super.getXStream();

        xstream.registerConverter(new CollectionConverter(xstream.getMapper()) {
            @Override
            @SuppressWarnings("rawtypes")
            public boolean canConvert(Class type) {
                return Collection.class.isAssignableFrom(type);
            }
        });

        return xstream;
    }

}

Criamos um Component que extende XStreamJSONSerialization. Ela recebe suas dependências e sobrescreve o método getXStream().

Recuperamos o método pai “original” e registramos um conversor através do método registerConverter. Este método recebeu uma instância de CollectionConverter que também terá o método canConvert sobrescrito, que por sinal tem o nome bem sugestivo.

Agora nossas coleções só serão serializadas se forem coleções de fato, evitando a tentativa da serialização de um Proxy. Você pode registrar quantos Converters quiser, por exemplo, um para trabalhar com Map:

    xstream.registerConverter(new MapConverter(xstream.getMapper()) {
        @Override
        @SuppressWarnings("rawtypes")
        public boolean canConvert(Class type) {
            return Map.class.isAssignableFrom(type);
        }
    }

Pronto! Agora você já pode serializar suas listas tranquilamente. (:

Esse problema já foi solucionado na versão 3.1.3, mas vale a pena pelo aprendizado. (;

Link do projeto:

http://github.com/wbotelhos/vraptor-3-evitando-circularreferenceexception-do-xstream

Upload e Download com VRaptor 3

É normal nos cadastrarmos em um sistema e lá ter uma opção de enviar sua própria foto. Outra opção mais do que usada é o simples download de algum arquivo. Iremos ver como o VRaptor nos auxiliam nessas tarefas e nos poupam um bocado de código e tempo.

Objetivo:
Criar uma funcionalidade na qual o usuário pode enviar uma foto para o sistema como seu avatar e logo em seguida fazer a apresentação desta imagem na tela.

Criando o usuário (Usuario.java):

public class Usuario {

    private Integer id;
    private String foto;

    // get and set...

}

Precisamos do ID do usuário para ser usado como identificador na foto e um campo String para mantermos o nome da foto.

Criando a sessão do usuário (UserSession.java):

@Component
@SessionScoped
public class UserSession {

    private Usuario user;

    // getters and setters...

}

Para trabalharmos com o avatar do usuário logado, precisamos ter este usuário em sessão, logo criamos tal componente que é SessionScoped.

Criando a classe de negócios (UsuarioBusiness.java):

Salvando a imagem do usuário:

public void salvarFoto(UploadedFile imagem) throws IOException {
    Usuario user = userSession.getUser();

    if (imagem != null) {
        String fileName = imagem.getFileName();

        int start = fileName.lastIndexOf(".");
        String extensao = (start > 0) ? fileName.substring(start) : ".jpg";

        user.setFoto(user.getId() + extensao);

        try {
            IOUtils.copy(imagem.getFile(), new FileOutputStream(new File(PATH_FOTO + "/" + user.getFoto())));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new FileNotFoundException("Arquivo não encontrado!");
        } catch (IOException e) {
            e.printStackTrace();
            throw new IOException("Não foi possível enviar o arquivo!");
        }
    }

    this.atualizarFoto(user);
    userSession.setUser(user);
}

Linha 5-10:
É recuperado o nome do arquivo e em seguida sua extensão que é concatenado com o ID do usuário. Depois é colocado de volta este novo nome na entidade para que seja persistida.

Usamos o ID do usuário como o nome do avatar porque o mesmo não se repete e facilita para localização.

Linha 13:
É utilizado o método copy da biblioteca commons-io da Apache para pegar a stream de entrada da imagem e jogar para uma stream de saída em um local físico.

Armazene as imagens em uma pasta fora da sua aplicação, pois assim você evita ter que fazer backup a cada redeploy, já que isto apaga todo conteúdo da sua aplicação.

Linha 23-24:
É feita a atualização dessa imagem no banco e reposto o usuário na sessão.

Removendo a imagem do usuário:

public void removerFoto() {
    Usuario user = userSession.getUser();

    if (user.getFoto() != null && !user.getFoto().isEmpty() && !user.getFoto().equalsIgnoreCase("default.jpg")) {
        File file = new File(PATH_FOTO + "/" + user.getFoto());

        if (file.exists()) {
            file.delete();
        }
    }

    user.setFoto("default.jpg");

    this.trocarFoto(user);
    userSession.setUser(user);
}

Linha 4:
É verificado se o usuário esta usando uma imagem e se esta não é a imagem padrão (default.jpg).

É importante verificar se o usuário não esta usando a imagem padrão, pois se esta for removida todos os usuários com imagem padrão serão prejudicados, já que todos apontam para a única e mesma imagem.

Linha 5-9:
É recuperado a imagem e verificado se a imagem existe, se sim, a mesma é removida.

Linha 12:
Como não deixamos o usuário sem foto, é setado a imagem padrão para o mesmo.

Linha 14-15:
É feito a atualização do nome da nova imagem no banco e reposto o usuário na sessão.

Método usado para o download:

public File downloadFoto() {
    File file = new File(PATH_FOTO + "/" + userSession.getUser().getFoto());
    return (file.exists()) ? file : new File(PATH_FOTO + "/default.jpg");
}

Procuramos a imagem do usuário, se ela existir é retornada, senão retornamos a imagem padrão.

Criando o controlador (UsuarioController.java):

Recebendo a imagem do usuário:

@Post
@Path("/usuario/foto")
public void atualizarFoto(UploadedFile imagem) {
    try {
        business.atualizarFoto(imagem);
    } catch (IOException e) { // Sua exception...
        result.include("error", e.getMessage()).forwardTo(this).upload();
    }
    result.redirectTo(this).upload();
}

Nosso método espera como argumento um UploadedFile que, de fato, é a imagem enviada pelo usuário através do formulário. E então a passamos para a nossa classe de negócios salvá-la em disco.

Ahm? E cadê o servlet com meus objetos ServletFileUpload, DiskFileItemFactory e etc para fazer esse upload?

Esquece isso, o VRaptor já tem tudo “mastigado” na classe UploadedFile que intercepta o envio do formulário com a imagem e já faz isso tudo por você. Basta manipulá-la como uma stream. (:

Estou ouvindo você falar: “Maldito servlet de upload que eu fazia na unha.”

Removendo a imagem do usuário:

@Get
@Path("/usuario/foto/remover")
public void atualizarFoto() {
    business.removerFoto();
    result.redirectTo(this).upload();
}

O método para remover a imagem simplesmente chama o nosso método já construido na nossa classe de negócios.

Fazendo o download da imagem:

@Get
@Path("/usuario/foto")
public File downloadFoto() {
    return business.downloadFoto();
}

No download temos um outra facilidade, já que precisamos apenas de retorna um File que o VRaptor já repassa para a view que iremos ver como irá ficar a seguir.

Criando a página de envio da foto (upload.jsp):

Para apresentar nossa foto na view, iremos fazer algo que até o dia em que o Makoto me falou eu não sabia.

Apresentando a imagem do download:

<a href="<c:url value='/usuario/foto'/>">
    <img src="<c:url value='/usuario/foto'/>" border="0"/>
</a>

Adicionamos diretamente como atributo a URL do nosso controller que nos retorna um File, se lembra? Então você me pergunta: “tá, eu tenho um File e não o caminho da imagem”.

O caminho direto não importa, sua aplicação não achará nenhum caminho fora dela mesmo, então fazemos um download e recuperamos essa imagem como uma stream, e esta pode ser setada diretamente no src do componente da imagem. :o

Se você não quiser fazer download da imagem terá de deixá-la dentro da sua aplicação.

Upload da imagem:

<form action="<c:url value="/usuario/foto"/>" enctype="multipart/form-data" method="post">
    Imagem: <input type="file" name="imagem"/> <input type="submit"/>
</form>

Nosso formulário submete a foto para a URL do método que a atualiza em nosso controller via POST. Precisamos de habilitar o envio de “anexo” do nosso formulário com o atributo enctype como multipart/form-data. Não se esqueça!

Remoção da imagem:

<a href="<c:url value="/usuario/foto/remover"/>">Remover</a><br/>

Para remover a imagem basta chamar o link de remoção e o usuário passará a ter a foto padrão.

Agora que você já sabe fazer upload e download de arquivos, irei listar algumas melhorias que eu costumo fazer em minhas aplicações que não cabe mostrar aqui neste artigo, mas que poderia ser uma segunda parte no futuro:

- Fazer o upload e download via ajax usando o jQuery Form;
- Validar a extensão ainda na view com o jQuery Validation;
- Validar a extensão também server side;
- Aumentar ou diminuir o limite do upload;
- Retorna uma imagem de erro caso ocorra uma exception;
- Integrar com o Gravatar.

Link do projeto:
http://github.com/wbotelhos/upload-e-download-com-vraptor-3