Neste artigo veremos o uso de um método muito interessante para codificação de dados, o Base64. Usaremos tal método para armazenar uma imagem no banco de dados e depois mostrá-la na página do usuário, usando JSF.

Base64 é um método comumente utilizado na internet para transferência de dados, usando a codificação MIME, o seu principal propósito é o envio de dados binários em meios que só podemos trafegar texto, como é o caso de anexos de e-mail. É simples e complexo ao mesmo tempo: Simples porque ele transforma o método convencional de envio de arquivos, que seria salvar fisicamente em um diretório para só depois realizar o download do mesmo, em algo mais rápido e simples, convertendo tudo em texto. Complexo porque por trás de toda esta facilidade está o funcionamento do algoritmo e como ele funciona.

Este é constituído por 64 caracteres ([A-Za-z0-9], "/" e "+") e por isso mesmo que seu nome é “Base 64”. Trabalha com transformação em bits a partir de um outro padrão, como por exemplo ASCII. Vejamos um exemplo de codificação da palavra “Man” na Tabela 1.

Conteúdo

M

A

N

ASCII

77

97

110

Padrão em Bit

01001101

01100001

01101110

Tabela 1. Exemplos de codificação

Agora os valores em bit são convertidos para decimal e posteriormente o algoritmo base 64 entende estes decimais como índices para serem convertidos a um os 64 caracteres, como mostra o exemplo a seguir:


  Bits          Decimal        Base 64        
  0 1 0 0 1 1 = 19               T
  0 1 0 1 1 0 = 22               W
  0 0 0 1 0 1 = 5                F
  1 0 1 1 1 0 = 46               u

Perceba que podemos converter qualquer tipo de informação para base 64 já que ele trabalha com binário e isto é comum a todo tipo de dado. Na Tabela 2 você pode conferir a tabela completa de conversão para base64.

Value Encoding

Value Encoding

Value Encoding

Value Encoding

0 A

17 R

34 i

51 z

1 B

18 S

35 j

52 0

2 C

19 T

36 k

53 1

3 D

20 U

37 l

54 2

4 E

21 V

38 m

55 3

5 F

22 W

39 n

56 4

6 G

23 X

40 o

57 5

7 H

24 Y

41 p

58 6

8 I

25 Z

42 q

59 7

9 J

26 a

43 r

60 8

10 K

27 b

44 s

61 9

11 L

28 c

45 t

62 +

12 M

29 d

46 u

63 /

13 N

30 e

37 v

(pad) =

14 O

31 f

48 w


15 P

32 g

49 x


16 Q

33 h

50 y


Tabela 2. Tabela Binária

Convertendo imagens em Base 64

Vimos que conseguimos trabalhar com qualquer tipo de dado através de string convertendo para o seu tipo original se necessário.

Vamos abstrair alguns pontos mais específicos de configurações do servidor, conexão ao banco e etc., dando enfoque ao uso do Base 64. O que precisamos inicialmente é da nossa tabela que irá gravar a imagem, como mostra a Listagem 1.

Listagem 1. Criação da tabela no banco de dados


  CREATE TABLE imagem_base_64
  (
    id serial NOT NULL,
    formato character varying(255),
    nome character varying(255),
    b64 text,
    CONSTRAINT imagem_base_64_pkey PRIMARY KEY (id )
  )
  WITH (
    OIDS=FALSE
  );
  ALTER TABLE imagem_base_64
    OWNER TO postgres;

O campo b64 irá armazenar o conteúdo em base 64 no formato text (escolhemos text porque iremos guardar uma grande quantidade de caracteres e o varchar limitaria muito nosso tamanho máximo), o formato irá guardar se a imagem é JPG, PNG, BITMAP e etc. Por último o campo nome grava o nome original do arquivo para que possamos gerar ele com o mesmo nome que foi salvo.

Nosso bean segue o mesmo modelo da tabela, como mostra a Listagem 2.

Listagem 2. Bean ImagemBase64


  public class ImagemBase64 {
         
         private String b64;
         private String formato;
         private String nome;
   
        public String getB64() {
               return b64;
         }
         public void setB64(String b64) {
               this.b64 = b64;
         }
   
         public String getFormato() {
               return formato;
         }
         public void setFormato(String formato) {
               this.formato = formato;
         }
         public String getNome() {
               return nome;
         }
         public void setNome(String nome) {
               this.nome = nome;
         }      
  }

A ideia do nosso projeto é possibilitar que o usuário envie uma imagem e esta seja convertida para base 64 e então salva no banco de dados.

Em nosso XHTML o usuário enviará o arquivo e em nosso ManagedBean , que será recebido e convertido para Base 64. Vejamos primeiramente nosso XHTML na Listagem 3.

Listagem 3. Definição do XHTML


  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml"
         xmlns:h="http://java.sun.com/jsf/html"
         xmlns:f="http://java.sun.com/jsf/core"
         xmlns:ui="http://java.sun.com/jsf/facelets">
   
  <h:head>
  </h:head>
  <h:body>
    <h:form enctype="multipart/form-data">
       <h:inputFile value="#{base64MB.imagem}" />
       <h:commandButton action="#{base64MB.salvar()}" value="Enviar" />
    </h:form>  
  </h:body>
  </html>

Nosso formulário é bem simples possuindo apenas um inputFile e um commandButton para submeter as informações ao ManagedBean. O uso do “enctype=multipart/form-data” é essencial para que este tipo de dado possa ser enviado pela requisição.

O commandButton dispara uma action chamada salvar() que veremos mais adiante em nosso ManagedBean.

Listagem 4. Definição do ManagedBean


  import java.io.IOException;
   
  import javax.faces.bean.ManagedBean;
  import javax.faces.bean.ManagedProperty;
  import javax.faces.bean.ViewScoped;
  import javax.servlet.http.Part;
   
  import org.bouncycastle.util.encoders.Base64;
   
  import br.com.myproject.bean.ImagemBase64;
  import br.com.myproject.util.Persist;
   
  @ManagedBean(name = "base64MB")
  @ViewScoped
  public class Base64ManagedBean {
         
         private Part imagem;
         
         @ManagedProperty(value = "#{persist}")
         private Persist per;
         
  public void salvar(){
               String formato = imagem.getContentType();
               String nome = imagem.getName();
               byte[] imageAsByte = new byte[(int) imagem.getSize()];
               try {
                      imagem.getInputStream().read(imageAsByte);
                      ImagemBase64 ib4 = new ImagemBase64();
                      String base64AsString = new String(Base64.encode(imageAsByte));
                      ib4.setB64(base64AsString);
                      ib4.setFormato(formato);
                      ib4.setNome(nome);
                      bo.save(ib4);
                      
                      System.out.println(ib4.getB64());
               } catch (IOException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
               }
         }
   
         public Part getImagem() {
               return imagem;
         }
   
         public void setImagem(Part imagem) {
               this.imagem = imagem;
         }
   
         public Persist getPer() {
               return per;
         }
   
         public void setPer(Persist per) {
               this.per = per;
         }
         
         
  }

Vamos entender o funcionamento do código da Listagem 4 em partes:

  • Primeiramente temos o nosso objeto “private Part imagem” que em linhas gerais irá gravar a imagem que enviamos através do request do XHTML. Explicaremos mais à frente a função do javax.servlet.http.Part, por enquanto basta sabermos que esta classe é a responsável por receber o conteúdo enviado pelo XHTML;
  • Logo abaixo temos a definição de um objeto do tipo Persist:
      @ManagedProperty(value = "#{persist}")
           private Persist per;
    Este objeto é apenas uma classe responsável por realizar a inserção do conteúdo no banco de dados, sendo que no momento não nos importa saber como essa inserção é realizada mas sim que ela deve ser feita de alguma forma, seja com Hibernate, JPA, JDBC ou qualquer outra técnica que você esteja adotando para inserções no seu projeto.
  • Não esqueça que o get() e set() de ambas as propriedades são obrigatórios para que possamos acionar a operação de salvar() sem problemas, como mostra o código a seguir:
    
    public Part getImagem() {
                 return imagem;
           }
     
           public void setImagem(Part imagem) {
                 this.imagem = imagem;
           }
     
           public Persist getPer() {
                 return per;
           }
    
    
           public void setPer(Persist per) {
                 this.per = per;
           }
  • Por fim, chegamos ao método que realmente nos importa, o salvar(). Capturamos algumas informações importantes como o tipo do conteúdo e o nome:
    
    String formato = imagem.getContentType();
                 String nome = imagem.getName();
    Sendo que o conteúdo é formatado da seguinte maneira: image/png, image/jpg e etc. Depois criamos um array de bytes com o tamanho da imagem com o método getSize():
    byte[] imageAsByte = new byte[(int) imagem.getSize()];

Assim garantimos que teremos espaço suficiente para armazenar o tamanho que a imagem necessitar. Em seguida, precisamos ler o conteúdo em bytes presente do objeto “imagem” e salvá-lo em nosso array “imageAsByte”:

imagem.getInputStream().read(imageAsByte);

O getInputStream().read() garante o armazenamento dos bytes em nosso array.

A classe org.bouncycastle.util.encoders.Base64 é a responsável por transformar nosso array de bytes em uma String formatada em Base64. São dois os método utilizados, encode() e decode(), onde o primeiro codifica a informação para Base64 e o segundo decodifica essa informação de Base64 para o valor original.

Nas linhas seguintes realizamos este processo:

ImagemBase64 ib4 = new ImagemBase64();
                      String base64AsString = new String(Base64.encode(imageAsByte));
                      ib4.setB64(base64AsString);
                      ib4.setFormato(formato);
                      ib4.setNome(nome);

Criamos nosso objeto do tipo ImagemBase64 e o hidratamos com o conteúdo necessário. O mais importante aqui é notar a seguinte linha:

 String base64AsString = new String(Base64.encode(imageAsByte));

Esta linha converte um array de bytes e outro array de bytes, a diferença está que este segundo array de bytes é em base 64, mas como precisamos gravar uma String no banco e não um array de bytes, nós usamos o “new String()” para converter este array de bytes para String.

Por fim nós chamamos o método que irá salvar o conteúdo no banco de dados e depois mostramos o valor em base 64 que armazenamos. Não iremos mostrar o resultado completo da linha:

System.out.println(ib4.getB64());

Pois este retorna uma grande quantidade de dados, que daria no mínimo 10 páginas só de caracteres em base64.

Veja uma parte dele (para a imagem que passamos), como mostra a Listagem 5.

Listagem 5. Parte do retorno do ib4.getB64()

/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAKGBQ4DASIAAhEBAxEB/8QAHQABAAMAAwEBAQAAAAAAAAAAAAUGBwMECAIBCf/EAFoQAAEDBAEDAgQCBwYEAgUAGwECAwQABQYREgcTISIxFBVB0lFVIzJTYZKUlQgWM0JxgRckUmJDkSUmNGOzcnN0ocHw8QkYNZaisdQ4R1ZldYKlsrTF0dPh/8QAGgEBAQEBAQEBAAAAAAAAAAAAAAECAwQFBv/EAD4RAAIBAQQJAwIEBQMFAAMBAAABEQIhMUHwElFhcZGhscHRAyKBBOEywtLxBRNCorJSYuIUI3KCkgYk8hX/2gAMAwEAAhEDEQA/APXVKVSctysMrWzBkLQ20dKcaAK3Vb1xTsaCQfr+P4AHfm+p+p9P6ejTrPT9P9PX9RXo0F2pVbg5dY0QY6Zt4jqkhpIeUlCgkr0ORHj23up+JIYlxm5MV5t5lxPJDjagpKh+IIrfp+tR6n4XJn1PQ9T0/wASa+DlpXw44htHJxQSn8TXVlTWuwrtPAL+nj/zrocju0roQJocUGnFbJ/VUfr+4/vrv0ApVYzfJFWZCI0bj8S4nmVKG+2n2Gh9VEg6+ngnzrRqE/IMotklKJUiYw6413QHUtK0k7AJSB48g+PSfFez0vovU9WmU0pPL6v1dHpuHLNWrr3CUmHFU8pClnYSlKfdSiQAP9yRUPgFwk3LHUyJTynlh5xAcUACpIUdb1+7xXfyD/2Jr/5rj/8AxU15vVofp1Ol4Hf061XSqlicmr+fIt0EfuMs7/8AnIpxyD8vgfzivsruZI++xb2zGeUw47KYZ7iQklIW6lJ1yBG9E+4qFu1yuFkuUZZmTrhFWy4p1pUQOEqBRxSktNjiSCsjl4PHXjexmDUne45B+XwP5xX2U45B+XwP5xX2VMwpLMyGzLjr5svIS42rWtpI2PFVzFcrRcLdfJ93XDt8a2XWTDLq3OCA20oAKUpR0Cd/6Ujz08ifGeB2uOQfl8D+cV9lOOQfl8D+cV9lSNvu1quNt+ZW+5wpcHRPxLD6VtaHv6gdeP8AWukjLsUctbt1Rk9kVb2XA27KE9otNrPslS+Wgf3E0iBJx8cg/L4H84r7Kccg/L4H84r7K6mSZY1DYx2VZ3YVxi3e6tQu+26Fo4LCyVIUk6J9P+ldfD88tV5ut0tEy42uLc4t0kQ2IXxSQ+6hs6C+BPI78+w14oqW86o8oNxnf4ZJ8cg/L4H84r7Kccg/L4H84r7K7r1+sTN4RZnb1bm7m4NohqlID6h+5G+R/wDKvyRf7DGvDVmkXu2s3N7XahuSkJfXv20gnkf9hSBJ0+OQfl8D+cV9lOOQfl8D+cV9ldXHM7xy9Trlb27lEjT7fKfjuxHpLYe00rSnOG9hH12alLhfI8V9DDMeTOcUz3ymKlKuLf8A1EkgaP01smpFiYm1rUdXjkH5fA/nFfZTjkH5fA/nFfZXJIySChuOqMxLml9j4gJjtglLX/UrZGv9Pf8AdX1IyKGkM/CsSp5dY+IAjIBKW/oo8iP/AC9/HtVgScPHIPy+B/OK+ynHIPy+B/OK+yuSRkkFDcdUZiXNL7HxATHbBKWv+pWyNf6e/wC6pWHJZmRG ...

Esta é apenas 1% do conteúdo total da nossa imagem enviada. O interessante aqui é que se colocássemos todo o conteúdo da nossa imagem em Bas64 bastaria você copiar toda a listagem e realizar o “decode()” seja qual for a linguagem que você está trabalhando (Java, PHP, Python, Ruby …) e a imagem será gerada, pois a linguagem pode ser diferente mas o algoritmo é o mesmo.

Perceba que estamos trabalhando apenas com imagens mas nosso escopo é totalmente adaptado para trabalhar também com qualquer tipo de arquivo: PDF, txt, MP4, AVI, JPG, PNG e qualquer outro tipo de arquivo, pois a ideia sempre será a mesma: Capturar o conteúdo em bytes e converter para base64 e enviar para onde quer que seja, em nosso caso estamos salvando em um banco de dados mas poderíamos enviar para um outro sistema via XML, JSON ou mesmo chamando um Webservice, ou via HTTP e etc. O leque de possibilidades é enorme e quando começamos a trabalhar com este “maravilhoso” algoritmo percebemos que tudo fica mais simples e prático e não mais precisamos criar estruturas supercomplexas para salvar arquivos enviados pelo usuário.

Prós e Contras

Infelizmente o algoritmo de Base 64 não é uma “bala de prata”, que mata qualquer coisa, ele possui seus pontos negativos.

Um dos pontos negativos de trabalhar-se com este tipo de método é a grande quantidade de informações que são armazenadas no banco de dados, fazendo com que as buscas tornem-se mais lentas e consequentemente a construção do arquivo original também, diferente do que ocorreria se apenas capturássemos o arquivo de um diretório onde ele já está salvo. Imagine um arquivo muito grande, de 1 GB por exemplo, provavelmente você teria sérios problemas se tentasse carregar as informações convertidas em Base 64 deste tamanho, o ideal seria quebrar o arquivo em blocos menores para juntá-lo posteriormente, consequentemente trazendo maior complexidade ao projeto.

Faça um teste você mesmo, converta um vídeo seu de 500MB para Base 64 e depois tente copiar o conteúdo em algum local, você verá uma grande lentidão devido a estar tentando carregar 500MB de informações dentro da memória de uma só vez.

Um outro ponto que você notará é o aumento em aproximadamente 33% do tamanho original do arquivo após conversão para base64, isso pode ter um peso considerável na transferência de dados através de uma rede. Além disso a conversão de grandes arquivos em base 64 e vice-versa é custado do ponto de vista do processamento exigido pela CPU.

A grande vantagem em se usar o algortimo de base 64 está na praticidade e flexibilidade quando trabalhamos com comunicação entre sistemas distintos, este padroniza a comunicação entre os sistemas assim como o XML. Antes você teria que fazer uma estrutura complexa de armazenado de arquivos em um diretório, salvando um apontador para este arquivo de forma que você possa recuperá-lo com agilidade depois, mas com base64 nada disso é necessário pois você terá todo arquivo em uma String, não que isso seja a melhor solução mas com certeza é a mais prática.

O uso da classe javax.servlet.http.Part

Em seções anteriores usamos a classe javax.servlet.http.Part para realizar a transferência do arquivo via HTTP. No JSF 2.2 foi introduzido o componente inputFile que juntamente com o servlet 3.0 (que disponibiliza a classe javax.servlet.http.Part) nos possibilita manipular o envio de arquivos via HTTP sem a necessidade de bibliotecas terceirizadas, como primefaces, omnifaces, icefaces e etc.

Para que seja possível usar a classe javax.servlet.http.Part você precisa adicionar a biblioteca servlet-api.jar versão 3.0 em seu projeto, há duas formas de fazer isso:

  1. Se você utilizar Maven, então basta adicionar o código da Listagem 6 no seu arquivo pom.xml.
    Listagem 6. Adicionando servlet-api.jar no pom.xml
    
    <dependency>
           <groupId>javax.servlet</groupId>
           <artifactId>servlet-api</artifactId>
           <version>3.0-alpha-1</version>
    </dependency>
    É obrigatório que a versão seja 3.0 ou superior.
  2. Outra forma é adicionar manualmente ao seu build path. Clique com o botão direito no seu projeto e vá até a opção “Properties”. Na tela que abrir procure por “Java Build Path” e vá até a aba “Libraries”.
    Clique em “Add External JARs...” e procure pela biblioteca servlet-api.jar, que deve estar dentro da basta “lib” do seu tomcat.
    Feito isso, seu projeto estará apto para usar a classe javax.servlet.http.Part. A classe Part na verdade não é uma classe e sim uma Interface, que representa o arquivo enviado como parte de um request multipart/form-data. Por isso é importante que o seu form tenha o “enctype” definido como “multipart/form-data”:
      <h:form enctype="multipart/form-data">

Quando tentamos submeter um formulário, ou seja, processar uma requisição sem a definição acima, temos o seguinte erro:

org.apache.tomcat.util.http.fileupload.FileUploadBase$InvalidContentTypeException: the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is application/x-www-form-urlencoded

De forma resumida, esse erro indica que é necessário o uso do multipart/form-data ou multipart/mixed para envio de um formulário que possua um arquivo, seja ele de qualquer tipo.

Quando usar o Base 64? Não existe uma regra geral para uso deste método, o que irá definir sua necessidade ou não são as regras de negócio do seu sistema, assim como ocorre com a maioria dos recursos.

Geralmente este é usado quando necessita-se enviar conteúdo (arquivos) para outros sistemas com linguagens ou estruturas diferentes, como ele trabalha com conversão em nível de bits, não importa qual linguagem está recebendo os dados. Um outro uso menos comum é a necessidade de armazenamento de arquivos no banco de dados, ou até mesmo dentro de uma classe Java (se o arquivo não for muito grande). Imagine que você precisa carregar um arquivo “.ddc” de 1KB toda vez que o usuário abrir o sistema, assim você poderia usar o algoritmo de base 64 para armazená-lo dentro do seu próprio código apenas recriando-o a cada abertura do sistema. Mas tenha cuidado, pois não é uma boa prática guardar arquivos em base64 dentro do próprio código, isso pode tornar seu sistema lento e extremamente grande (em termos de espaço físico consumido).