Archive

Archive for the ‘Programação’ Category

Como Integrar o Apache Shiro a uma Aplicação JSF

O Shiro é um framework de segurança da Apache escrito em Java que permite a criação de soluções para autenticação, autorização, criptografia, gerenciamento de sessão e ainda fornece várias implementações de cache. Ele é simples de configurar e tem vários pontos de extensão que nos permitem intervir sobre seu funcionamento – são características de um framework bem projetado -, mas as configurações padrão já são suficientes para uma aplicação de pequeno porte.

Figura 1 – Arquitetura do Shiro

Nesse artigo, vou mostrar um exemplo de como integrar o Shiro a uma aplicação JSF. Nosso modelo será composto por uma enum chamada Permissao, uma enum Perfil para agrupar as permissões, e a entidade Usuario, que possui um Perfil:

public enum Permissao {
   CRIAR_PEDIDO, 
   LISTAR_PEDIDOS;
}

public enum Perfil {
   ADMINISTRADOR(Permissao.CRIAR_PEDIDO, Permissao.LISTAR_PEDIDOS);
   private List<Permissao> permissoes;
   private Perfil(Permissao... permissoes) {
      this.permissoes = Arrays.stream(permissoes).
         collect(Collectors.toList());
   }
}

@Entity
@Table(name = "TBL_USUARIO")
@TypeDefs(value = {@TypeDef(name = "TIPO_PERFIL" ...)})
public class Usuario {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   @Column(name = "PK_USUARIO")
   private Long id;
   @Column(name = "NM_USUARIO")
   private String nome;
   @Column(name = "COD_PERFIL")
   @Type(type = "TIPO_PERFIL")
   private Perfil perfil;
}

Há várias formas de configurar o Shiro. Vou mostrar como configurá-lo à partir do arquivo de configuração shiro.ini. Precisamos incluir duas dependências no arquivo pom.xml – isso é o básico para o Shiro ser integrado a uma aplicação web:

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-core</artifactId>
   <version>1.3.2</version>
</dependency>
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-web</artifactId>
   <version>1.3.2</version>
</dependency>

Em seguida, vamos configurar o arquivo shiro.ini, que deve ficar no classpath – eu costumo colocar em /src/main/resources. As seções desse arquivo agrupam propriedades que determinam o comportamento do Shiro. Basicamente, informamos qual é o Realm utilizado (será explicado mais adiante), qual é a página de autenticação para a qual o Shiro redirecionará o usuário não autenticado, para onde o usuário será redirecionado após o logout e também permite que informemos que há páginas privadas que só podem ser acessadas por usuários que tenham determinadas permissões. No nosso exemplo, a página “criarpedido.xhtml” só pode ser acessada por usuários que contenham a permissão “CRIAR_PEDIDO”.

[main]
apprealm = br.com.app.autorizacao.AppRealm
securityManager.realms = $apprealm
authc.loginUrl = /publico/index.xhtml
logout.redirectUrl = /publico/index.xhtml

[users]

[roles]
ADMINISTRADOR = *

[urls]
/privado/criarpedido.xhtml =  authc, perms["CRIAR_PEDIDO"]

No arquivo web.xml, configuraremos os filtros e listeners do Shiro e também informaremos quais são as páginas que responderão pelos códigos de erro HTTP.

<filter>
   <filter-name>ShiroFilter</filter-name>
   <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>

<filter-mapping>
   <filter-name>ShiroFilter</filter-name>
   <url-pattern>/*</url-pattern>
   <dispatcher>REQUEST</dispatcher>
   <dispatcher>FORWARD</dispatcher>
   <dispatcher>INCLUDE</dispatcher>
   <dispatcher>ERROR</dispatcher>
</filter-mapping>

<listener>
   <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener
</listener-class>
</listener>
	
<error-page>
   <error-code>401</error-code>
   <location>/publico/acessonegado.xhtml</location>
</error-page>

<error-page>
   <error-code>403</error-code>
   <location>/publico/acessonegado.xhtml</location>
</error-page>

<error-page>
   <error-code>404</error-code>
   <location>/publico/paginanaoencontrada.xhtml</location>
</error-page>

<error-page>
   <error-code>500</error-code>
   <location>/publico/erro.xhtml</location>
</error-page>

Essa é toda a configuração básica que qualquer aplicação web precisa para ser integrada ao Shiro. De agora em diante, começaremos a implementação do nosso mecanismo de autenticação e autorização. Só então você verá como é simples trabalhar com o Shiro. Uma peça importante do mecanismo de autenticação é o AuthenticationToken, que é a classe que mantém as credenciais do usuário. Você pode escolher uma das implementações dessa interface disponíveis, mas vamos criar uma implementação dela que recebe o ID do usuário:

public class IdAuthenticationToken implements AuthenticationToken {
   private Integer userId;

   public IdAuthenticationToken(Integer userId) {
      this.userId= userId;
   }

   @Override
   public Object getPrincipal() {
      return this.id;
   }

   @Override
   public Object getCredentials() {
      return this.id;
   }
}

Você precisará informar ao Shiro que um usuário está se autenticando. Você pode fazer isso à partir de um servlet, campos de input em um formulário de uma tela que é tratada por um ManagedBean, etc. Eu fiz através de um servlet porque antes que o usuário se autenticasse na minha aplicação era feita client authentication contra um Apache Server para validar o certificado digital. Modifiquei um pouco o código para indicar que as credenciais do usuário estão vindo pela request, que no meu caso era um post realizado pela aplicação que rodava em um JBoss debaixo daquele Apache. A credencial é apenas o atributo “userId”, que foi fornecido pela aplicação que validou o certificado digital.

@WebServlet(displayName = "AutenticacaoServlet", 
   name = "AutenticacaoServlet", urlPatterns = "/autenticar")
public class AutenticacaoServlet extends HttpServlet {

protected void doPost(HttpServletRequest request, 
      HttpServletResponse response)
     throws ServletException, IOException {
   Integer userId = (Integer) request.getAttribute("userId");
   Subject subject = SecurityUtils.getSubject();
   subject.login(new IdAuthenticationToken(token, userId));
   subject.checkPermission("CRIAR_PEDIDO");

   response.sendRedirect(request.getContextPath() + 
      "/private/criarpedido.xhtml?faces-redirect=true";

O Realm é um componente que interpreta os artefatos de segurança da aplicação – como usuários, regras e permissões – para o Shiro. Sendo o Realm a fonte de dados para autorização, podemos dizer, grosso modo, que ele é o DAO, ou melhor, um repository, que serve um modelo de autorização e autenticação.

Figura 2 – Sequência Simples de Autenticação

public class AppRealm extends SimpleAccountRealm {

   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(
       AuthenticationToken token) throws AuthenticationException {
      IdAuthenticationToken authToken = (IdAuthenticationToken) token;
      Optional<Usuario> usuario = usuarioDao.buscar(token.getUserId());
      return new SimpleAuthenticationInfo(
         usuario.get(), usuario.get().getId(), getName());
   }

   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(
        PrincipalCollection principals) {
      Set<String> roles = new HashSet<>();
      Set<String> permissions = new HashSet<>();

      Usuario usuario = (Usuario) SecurityUtils.getSubject().getPrincipal();
      Perfil perfil = usuario.getPerfil();

      roles.add(perfil.name());

      perfil.getPermissoes().forEach(a -> {
         permissions.add(a.nome());
      });

      SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
      info.setRoles(roles);
      info.setStringPermissions(permissions);
      return info;
   }

   @Override
   public String getName() {
      return getClass().getName();
   }

   @Override
   public boolean supports(AuthenticationToken token) {
      return IdAuthenticationToken.class.equals(token.getClass());
   }
}

Utilizamos a implementação SimpleAccountRealm, que é suficiente para o que nos propusemos a fazer. Se você quiser que o Shiro fique automaticamente consultando o banco de dados para carregar as permissões, basta utilizar um JdbcRealm e adicionar a consulta que o Shiro deve utilizar no arquivo shiro.ini.

Nesse momento, já temos um mecanismo que gerencia as credenciais do usuário e autoriza o acesso às páginas informadas na configuração inicial. Se você precisar fazer checagens de permissão ou verificar se o usuário está autenticado programaticamente, você deve acessar o Subject através do SecurityUtils ou utilizar as anotações do Shiro nos métodos das suas classes que devem ser autorizados.

Recomendações para Migração de Aplicações JSF “Puras”

Se você tem uma implementação de PhaseListener que de alguma forma toma decisões com base na URL acessada em sua aplicação JSF, remova-o. Na verdade, você deveria remover filtros, servlets e quaisquer outras implementações interessadas em autorização. Quem cuidará disso é o Shiro – mais especificamente a sua implementação de Realm.

É comum que aplicações JSF tenham uma ExceptionHandlerFactory para interceptar e tratar exceções nas páginas. Isso também não será necessário, pois o Shiro contém a ShiroException e suas variantes. Você também pode definir páginas específicas para tratar os códigos de erro HTTP mapeadas no seu arquivo web.xml.

Referências

1. [http://shiro.apache.org/reference.html]
2. [https://shiro.apache.org/java-authorization-guide.html]
3. [https://shiro.apache.org/authorization.html]
4. [https://shiro.apache.org/architecture.html]
5. [https://shiro.apache.org/configuration.html]
6. [http://galluzzo.com.br/?p=698]
7. [https://dzone.com/articles/java-security-framework-apache]
8. [http://czetsuya-tech.blogspot.com/2012/10/how-to-integrate-apache-shiro-with.html]
9. [http://www.javastutorial.com/2016/03/how-to-use-apache-shiroauthentication.html]

Como Gerar o Checksum de um Arquivo

Como expliquei no artigo Criptografia de Senha em Java, uma função de hash gera uma cadeia de caracteres de tamanho fixo a partir de uma sequência de qualquer tamanho; é uma equação matemática que a partir de um texto cria um código chamado message digest (resumo de mensagem), que é o resultado retornado por uma função de hash. Este pode ser comparado a uma impressão digital, pois cada documento possui um valor único de resumo e até mesmo uma pequena alteração no documento, como a inserção de um espaço em branco, resulta em um resumo completamente diferente.

Naquele artigo, mostrei como criptografar um texto utilizando o SHA-256, que é a variante mais utilizada do SHA-2. O código abaixo utiliza outros algoritmos (ou funções hash) para gerar o message digest do conteúdo de um arquivo qualquer. Quando esse resultado é utilizado para checar se um arquivo é genuíno e isento de erros, ele passa a ser chamado de hash sum ou mais popularmente de checksum:

public static void main(String[] args) throws Exception {
   byte[] fileBytes = Files.readAllBytes(Paths.get("/path/my_file.war"));
   Arrays.asList("MD5", "SHA-1", "SHA-256", "SHA-512").forEach((alg) -> {
      try {
         MessageDigest algoritmo = MessageDigest.getInstance(alg);
         byte digestMessage[] = algoritmo.digest(fileBytes);
         System.out.println(alg);
         System.out.println(new HexBinaryAdapter().marshal(digestMessage));
      } catch (NoSuchAlgorithmException e) {
         e.printStackTrace();
      }
   });
}

Utilizei o HexBinaryAdapter do Java, mas mas você poderia utilizar o Hex.encodeHexString(…) do Apache Commons ou ainda poderia converter os bytes para a versão hexadecimal manualmente byte-a-byte, assim:

(...)
StringBuilder hexPassword = new StringBuilder();
for (byte aByte : digestMessage) {
   hexPassword.append(String.format("%02X", 0xFF & aByte));
}
System.out.println(hexPassword.toString());

Alternativas

Se você só precisa do checksum e estiver utilizando Windows, você poderia baixar o FCIV da Microsoft, que pelo menos é um utilitário de linha de comando, ou o Hash Calculator, que mostra os checksums de forma gráfica. O FCIV é bem simples de utilizar:

C:\>fciv -SHA1 {ARQUIVO}

Referências

1. [http://howtodoinjava.com/core-java/io/how-to-read-file-content-into-byte-array-in-java/]
2. [http://www.adam-bien.com/roller/abien/entry/java_8_reading_a_file]
3. [http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest]
4. [http://www.devmedia.com.br/como-funciona-a-criptografia-hash-em-java/31139]
5. [https://www.lifewire.com/what-does-checksum-mean-2625825]

Injeção de Dependência no Java EE

O CDI (Contexts and Dependency Injection) é um recurso muito útil do Java EE embora a idéia seja bem mais antiga que o framework. Nesse artigo, trataremos do uso da anotação @Inject para permitir que possamos escolher qual das instâncias da interface PessoaService será injetada em um managed bean.

public interface PessoaService {}

Para esse exemplo, a interface PessoaService terá apenas duas implementações:

public class PessoaFisicaService implements PessoaService {}

public class PessoaJuridicaService implements PessoaService {}

Em princípio, vamos deixar o próprio container, que no caso é o JBoss, escolher a implementação que será injetada tomando por base a interface do atributo pessoaService no managed bean abaixo:

@ViewScoped
public class ManagedBean {
   @Inject
   private PessoaService pessoaService;
}

O código acima vai lançar uma exceção, pois ao delegarmos para o container a escolha de uma implementação que atenda à interface, ele não terá como decidir entre as duas implementações disponíveis. Você poderia injetar diretamente a implementação que você necessita, mas uma boa prática de design orientado à objetos e que considero importante quando se trabalha com linguagens fortemente tipadas como o Java é desenvolver “contra” a interface para que seja estabelecido um contrato que várias implementações terão que atender, o que permite que troquemos a implementação utilizada uma vez que a composição foi desacoplada – a classe onde a instância é injetada depende da interface (contrato) e não de uma implementação. Sendo assim, em princípio, vamos apenas qualificar nossas implementações com @Named:

@Named("PessoaFisica")
public class PessoaFisicaService implements PessoaService {}

@Named("PessoaJuridica")
public class PessoaJuridicaService implements PessoaService {}

Em seguida, basta informar o nome ou qualificação da implementação que queremos que o container injete. No caso, garantimos que a implementação injetada é PessoaFisicaService, que é qualificada ou nomeada como PessoaFisica:

@ViewScoped
public class ManagedBean {
   @Inject
   @Named("PessoaFisica")
   private PessoaService pessoaService;
}

O problema dessa estratégia é que ela não é “refactor friendly“, pois se você renomear uma implementação, terá que adequar esse nome em todos os lugares em que ela é utilizada. Não podemos depender de nossa memória, que em geral é bem estragada nos desenvolvedores de software. Também temos que pensar nos pobres coitados que darão manutenção naquele código no futuro. Uma forma elegante de resolver esse problema é criar uma anotação específica para cada implementação e anotá-las com @Qualifier. O @Qualifier indica ao container que ele deverá procurar uma implementação anotada com a anotação definida:

@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PessoaFisica{}

@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PessoaJuridica{}

Vamos aplicar essas anotações em nossas implememntações:

@PessoaFisica
public class PessoaFisicaService implements PessoaService {}

@PessoaJuridica
public class PessoaJuridicaService implements PessoaService {}

Em seguida, vamos indicar que a implementação do contrato que deve ser utilizada é @PessoaFisica:

@ViewScoped
public class ManagedBean {
   @Inject
   @PessoaFisica
   private PessoaService pessoaService;
}

O código parece bem limpo e você pode refatorá-lo pela sua IDE favorita sem que precise lembrar de alterar os lugares em que as implementações são utilizadas. Não é necessário incluir muitos comentários para que um desenvolvedor entenda a intenção de quem codificou e o que está acontecendo ali. O único problema é que para cada implementação você deve criar uma anotação, mas essa é uma decisão de projeto que você terá que tomar. Sempre converse com seus colegas antes de tomar esse tipo de decisão. Às vezes eles podem ter uma ideia melhor que a sua.

Referências

1. [http://sysout.be/2011/06/16/java-ee6-cdi-named-components-and-qualifiers/]

Categorias:Programação

Implementação de um Cliente RESTEasy

Escrevi um artigo para demonstrar como criar um mecanismo de autorização com token no RESTEasy. Fiz a implementação do lado do servidor e testei com o SOAPUI, mas nesse artigo vou demonstrar a implementação de um cliente Java que utiliza aqueles serviços que foram disponibilizados.

Primeiro, vamos criar as entidades do nosso modelo utilizando as anotações do Jackson. Você poderia trabalhar direto com String ou manipular o JSON manualmente, mas há boas bibliotecas para fazer essa conversão automaticamente. Vamos criar uma classe para representar a autenticação que será enviada ao método específico do endpoint e outra para representar a autorização, que é a resposta do endpoint que contém o token de autorização que deverá ser utilizado nas chamadas aos demais web services.

@JsonIgnoreProperties(ignoreUnknown = true)
public class Autenticacao {
	@JsonProperty("usuario")
	private String usuario;
	@JsonProperty("senha")
	private String senha;
}

@JsonIgnoreProperties(ignoreUnknown = true)
public class Autorizacao {
   @JsonProperty("token")
   private String token;
}

O primeiro método que criaremos é aquele que consome a autenticação e produz uma instância da classe Autorizacao a partir do JSON retornado pelo web service. Nosso endpoint responde no endereço http://localhost:8080/endpoint e faremos uma chamada POST para enviar os dados da autenticação no cabeçalho da request.

   public Autorizacao autenticar() throws Exception  {
      Autenticacao autenticacao = new Autenticacao();
      autenticacao.setUsuario("usuario");
      autenticacao.setSenha("senha");
      Entity<Autenticacao> entity = 
         Entity.entity(autenticacao, MediaType.APPLICATION_JSON);
      ResteasyClient client = new ResteasyClientBuilder().build();
      ResteasyWebTarget target = client.
         target("http://localhost:8080/endpoint").
         path("/autenticar/");
      Response response = target.request(MediaType.APPLICATION_JSON).
         post(entity);
      String value = response.readEntity(String.class);
      response.close();
      ObjectMapper map = new ObjectMapper();
      return map.readValue(value, Autorizacao.class);
   }

O header da request ficará mais ou menos assim:

POST http://localhost:8080/endpoint/autenticar/ HTTP/1.1
Accept-Encoding: gzip,deflate
Content-Type: application/json
Content-Length: 51
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.7)
{
	"usuario" : "admin",
	"senha" : "1234"
}

A resposta do web service será assim:

{
   "token": "81FE2DFA7A9A822D5EBA0A7B1ABA2CBDB7307B1D8A5869CDBEB64B47DA453AE3"
}

Agora que obtivemos um token que autoriza a utilização dos web services durante “T” unidades de tempo supondo que essa validade esteja implementada no servidor, vamos listar os usuários disponíveis com uma chamada GET cujo cabeçalho conterá um atributo “Token”.

@JsonIgnoreProperties(ignoreUnknown = true)
public class Usuario {
	@JsonProperty("nome")
	private String nome;
}
   public List<Usuario> listarUsuarios(Autorizacao autorizacao)
      throws Exception {
      ResteasyClient client = new ResteasyClientBuilder().build();
      ResteasyWebTarget target = client.
         target("http://localhost:8080/endpoint").
         path("/usuario/listar/");
      Response response = 
         target.request(MediaType.APPLICATION_JSON).
         header("Token", autorizacao.getToken()).get();
      String value = response.readEntity(String.class);
      response.close();
      ObjectMapper map = new ObjectMapper();
      Map<String, List<Usuario>> mapItens = map.readValue(value,
         new TypeReference<Map<String, List<Usuario>>>() {
      });
      return mapItens.get("usuarios");
   }

O header da request ficará assim.

GET http://localhost:8080/endpoint/usuario/listar/ HTTP/1.1
Accept-Encoding: gzip,deflate
Token: 81FE2DFA7A9A822D5EBA0A7B1ABA2CBDB7307B1D8A5869CDBEB64B47DA453AE3
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.7)

A resposta do web service será assim:

{
   "usuarios": [   
      { "nome": "José da Silva" },
      { "nome": "Mário de Souza" }
   ]
}

Por fim, vamos testar nosso cliente.

@Test
public void listarUsuarios throws Exception {
   WSClient client = new WSClient();
   Autorizacao autorizacao = client.autenticar();
   List<Usuario> usuarios = client.listarUsuarios(autorizacao);
   usuarios.stream().forEach((u) -> { System.out.println(u.getNome()); });
}

Referências

1. [https://atitudereflexiva.wordpress.com/2016/10/03/implementacao-de-autorizacao-com-token-utilizando-resteasy/]
2. [https://examples.javacodegeeks.com/enterprise-java/rest/resteasy/json-example-with-resteasy-jackson/]
3. [http://stackoverflow.com/questions/22095289/basic-authentication-with-resteasy-client]
4. [http://stackoverflow.com/questions/33241363/how-send-list-as-query-param-using-resteasy-client]
5. [http://www.concretepage.com/webservices/resteasy-3/jax-rs-resteasy-3-form-to-post-applicationx-www-form-urlencoded-content-type]
6. [http://howtodoinjava.com/resteasy/jax-rs-2-0-resteasy-3-0-2-final-client-api-example/]
7. [http://stackoverflow.com/questions/18520654/how-do-i-send-json-to-an-external-api-using-resteasy]

HTTP Proxy

Precisava acessar um recurso REST à partir do meu próprio web service para enviar um JSON com os dados de autenticação do usuário e ler um outro JSON que conteria os dados do usuário ou uma mensagem de erro. O proxy corporativo estava negando o acesso à URL daquele recurso.

A solução era mais simples do que pensei – para não dizer óbvia. Utilizei a classe Proxy em colaboração com a classe InetSocketAddress para fazer as configurações de proxy necessárias para que o objeto URL conseguisse abrir uma conexão com o destino:

      String enderecoProxy = "proxy.corporativo.com.br";
      Integer portaProxy = 3510;
      Proxy proxy = new Proxy(Proxy.Type.HTTP, 
         new InetSocketAddress(enderecoProxy, portaProxy));
      URL u = new URL("https://site.com.br/endpoint/autenticar");
      HttpsURLConnection connect = (HttpsURLConnection) u.openConnection(proxy);

Segue abaixo o exemplo completo que demonstra o problema e a solução:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;

import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.net.ssl.HttpsURLConnection;
import javax.servlet.http.HttpServletRequest;

@Path("/endpoint")
public class AutenticarResource {
   @GET
   @Path("/autenticar")
   @Produces(MediaType.APPLICATION_JSON)
   public void autenticar() throws IOException {
      HttpsURLConnection connect = configurarConexao();
      autenticar(connect);
      JsonObject json = extrairResposta(connect);
      logarDadosResposta(json);
   }
   
   private HttpsURLConnection configurarConexao() 
         throws MalformedURLException, IOException, ProtocolException {
      // Configura o proxy
      String enderecoProxy = "proxy.corporativo.com.br";
      Integer portaProxy = 3510;
      Proxy proxy = new Proxy(Proxy.Type.HTTP, 
         new InetSocketAddress(enderecoProxy, portaProxy));

      // Abre a conexao passando o proxy configurado
      URL u = new URL("https://site.com.br/endpoint/autenticar");
      HttpsURLConnection connect = (HttpsURLConnection) u.openConnection(proxy);
      connect.setDoOutput(true);
      connect.setRequestProperty("Content-Type", "application/json");
      connect.setRequestProperty("Accept-Charset", "utf-8");
      connect.setRequestMethod("POST");
      return connect;
   }

   private void autenticar(HttpsURLConnection connect) 
         throws UnsupportedEncodingException, IOException {
      JsonObject autenticarJson = 
         Json.createObjectBuilder().add("cpf", "111.111.111-11").build();
      OutputStreamWriter wr = 
         new OutputStreamWriter(connect.getOutputStream(), "utf-8");
      wr.write(autenticarJson.toString());
      wr.flush();
   }

   private JsonObject extrairResposta(HttpsURLConnection connect) 
         throws UnsupportedEncodingException, IOException {
      BufferedReader in = new BufferedReader(
         new InputStreamReader(connect.getInputStream(), "utf-8"));
      String out = in.readLine();
      JsonReader reader = Json.createReader(new StringReader(out));
      JsonObject json = reader.readObject();
      return json;
   }

   private void logarDadosResposta(JsonObject json) {
       JsonObject jsonRP = json.getJsonObject("resposta");
       if (jsonRP.getBoolean("sucesso")) {
          JsonObject usuarioJson = json.getJsonObject("usuario");
          System.out.println(usuarioJson.getString("nome"));
          System.out.println(usuarioJson.getInt("idade"));
       } else {
          System.out.println(jsonRP.getString("mensagemErro"));
       }
   }
}

Mapeamento de Oracle Types em Java

Uma das vantagens da orientação a objetos é a reutilização de código. Em PL/SQL, o equivalente à esse aspecto da orientação a objetos é o uso de tipos. Os tipos permitem a separação de interfaces e detalhes de implementação. Os tipos encapsulam estruturas de dados e as funções e procedures que as manipulam.

Vamos explicar o funcionamento de cada estrutura necessárias através da implementação de uma procedure que retorna um array de tipos Pessoa. Primeiro, vamos criar uma tabela para armazenar os dados das pessoas.

CREATE TABLE TBL_PESSOA
(
  PK     NUMBER(3) NOT NULL,
  NOME   VARCHAR2(64) NOT NULL,
  IDADE  NUMBER(2) NOT NULL
);

E em seguida vamos inserir alguns registros para validar a procedure e o tipo que serão criados.

INSERT INTO TBL_PESSOA
VALUES (1,'JOSÉ', 30);
INSERT INTO TBL_PESSOA
VALUES (2,'JOÃO', 35);

Vamos definir a interface do tipo TYPE_PESSOA. Note a função CONSTRUIR designada como STATIC. Métodos STATIC, diferente de métodos MEMBER, devem ser ivocados no tipo e não em uma instância. Esse método é apenas um facilitador para criar instâncias do tipo.

CREATE OR REPLACE TYPE TYPE_PESSOA AS OBJECT (
   PK    NUMBER(3),    -- Identificador
   NOME  VARCHAR2(64), -- Nome
   IDADE NUMBER(2),    -- Idade
   STATIC FUNCTION CONSTRUIR RETURN TYPE_PESSOA
);

Em seguida, vem a implementação ou BODY do tipo. Na implementação do tipo definimos o comportamento da função CONSTRUIR. Ela apenas instancia TYPE_PESSOA com valores nulos. Essa instância será preenchida na procedure que será definida posteriormente.

CREATE OR REPLACE TYPE BODY TYPE_PESSOA AS
   STATIC FUNCTION CONSTRUIR RETURN TYPE_PESSOA IS
     REC TYPE_PESSOA;
   BEGIN
     REC := TYPE_PESSOA(NULL,NULL,NULL);
     RETURN REC;
   END CONSTRUIR;
END;

Como nosso retorno será um array de registros de Pessoas, vamos definir um tipo auxiliar que armazenará TYPE_PESSOA em tabela durante a execução da procedure. Sendo assim, TYPE_PESSOA_LISTA é um array de TYPE_PESSOA.

CREATE OR REPLACE TYPE TYPE_PESSOA_LISTA AS
TABLE OF TYPE_PESSOA;

A última estrutura de banco de dados que precisaremos implementar é a procedure. Essa procedure busca pessoas e popula um TYPE_PESSOA para cada ocorrência e as armazena no tipo auxiliar TYPE_PESSOA_LISTA. Nessa implementação, usei labels e loops para navegar pelos registros, mas assumo que se você precisou criar um tipo, que é uma estrutura complexa, deve conhecer bem os camandos básicos, tratamento de erros – que não mostrei aqui – e como funciona a declaração de variáveis e atribuição de valores em PL/SQL.

 
CREATE OR REPLACE PROCEDURE PROCEDURE_BUSCAR_USUARIOS
   (P_USUARIO_LISTA OUT NOCOPY TYPE_PESSOA_LISTA)
IS

V_PK NUMBER(3);
V_NOME VARCHAR2(64);
V_IDADE NUMBER(2);

BEGIN
   V_PK := 0;
   V_NOME := '';
   V_IDADE := 0;
   P_USUARIO_LISTA := TYPE_PESSOA_LISTA();

   <<LABEL_PESSOA>>;
   FOR V_PESSOA IN (
      SELECT TP.PK, TP.NOME, TP.IDADE
         INTO V_PK, V_NOME, V_IDADE
         FROM TBL_PESSOA TP
      )LOOP

     V_PK := V_PESSOA.PK;
     V_NOME := V_PESSOA.NOME;
     V_IDADE := V_PESSOA.IDADE;
      
     P_USUARIO_LISTA.EXTEND(1);
     P_USUARIO_LISTA(P_USUARIO_LISTA.COUNT) := TYPE_PESSOA.CONSTRUIR();
     P_USUARIO_LISTA(P_USUARIO_LISTA.COUNT).PK := V_PK;
     P_USUARIO_LISTA(P_USUARIO_LISTA.COUNT).IDADE := V_IDADE;
     P_USUARIO_LISTA(P_USUARIO_LISTA.COUNT).NOME := V_NOME;
   END LOOP LABEL_PESSOA;

END PROCEDURE_BUSCAR_USUARIOS;

Agora vamos passar para a implementação do código Java. Comecemos pela definição de um POJO Pessoa.

public class Pessoa {
   private Integer id;
   private String nome;
   private Integer idade;
   // getters e setters
   public Pessoa(Integer id, Integer idade, String nome) {
      this.id = id;
      this.idade = idade;
      this.nome = nome;
   }
}

A interface SQLData é utilizada para implementar um mapeamento mais refinado e granular de um tipo de dado definido no banco de dados – não necessariamente aquele tipo que criamos – para uma classe Java. A implementação do SQLData receberá um SQLInput, que é um objeto entregue pelo driver do banco de dados e que permite acesso ou gravação de valores. Vamos definir uma classe que implementa um SQLData para facilitar a extração das informações que virão da procedure por meio daquele array de tipos que definimos.

import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;

public class PessoaOracleType implements SQLData {
   private int id;
   private String nome;
   private int idade;

   private String sqSQLDatal_type;

   public String getSQLTypeName() {
      return sql_type;
   }

   public int getId() {
      return id;
   }

   public String getNome() {
      return nome;
   }

   public int getIdade() {
      return idade;
   }

   public void readSQL(SQLInput stream, String type) 
      throws SQLException {
      sql_type = type;
      id = stream.readBigDecimal().intValue();
      nome = stream.readString();
      idade = stream.readBigDecimal().intValue();
   }

   public void writeSQL(SQLOutput stream) 
      throws SQLException {
      stream.writeInt(id);
      stream.writeString(nome);
      stream.writeInt(idade);
   }
}

Vamos utilizar a interface Work do Hibernate para ter acesso à Connection que será utilizada para ivocarmos a procedure criada. Uma classe curiosa que vai aparecer nessa implementação é a STRUCT, que é fornecida pelo driver do Oracle. Uma instância de STRUCT é a materialização de um objeto Oracle quando não há um mapeamento Java/SQL explícito. No nosso caso, cada linha do array de tipos retornado pela chamada da procedure será uma STRUCT.


import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.sql.rowset.serial.SQLInputImpl;

import oracle.jdbc.OracleTypes;
import oracle.sql.STRUCT;

import org.hibernate.Session;
import org.hibernate.jdbc.Work;

public class Procedure {
   private List<Pessoa> pessoas;
   private Session session;

   public Procedure(Session session) {
      this.session = session;
   }

   public List<Pessoa> getPessoas() {
      return pessoas;
   }

   public void execute() {
      session.doWork(new Work() {
         @Override
         public void execute(Connection con) throws SQLException {
            CallableStatement callStm = null;
            try {
               Map<String, Class<?>> map = con.getTypeMap();
               map.put("TYPE_PESSOA_LISTA", PessoaOracleType.class);
               callStm = con.prepareCall(
                 "{call PROCEDURE_BUSCAR_USUARIOS(?)}");
               registrarParametroSaida(callStm, "P_USUARIO_LISTA", 
                 OracleTypes.ARRAY, "TYPE_PESSOA_LISTA");
               callStm.execute();
               pessoas = extrair(callStm);
            } finally {
               if (callStm != null) {
                  callStm.close();
               }
            }
         }
      });
   }

   private void registrarParametroSaida(CallableStatement cs, 
     String nomeParametro, int oracleTypes, String typeName)
     throws SQLException {
     cs.registerOutParameter(nomeParametro, oracleTypes, typeName);
   }

   @SuppressWarnings("unchecked")
   private List<Pessoa> extrair(CallableStatement cs) 
      throws SQLException {
      List<Pessoa> pessoas = new ArrayList<Pessoa>();
      Array array = cs.getArray("P_USUARIO_LISTA");
      ResultSet rs = array.getResultSet();

      while (rs.next()) {
         STRUCT row = (STRUCT) rs.getObject(2);
         PessoaOracleType vot = new PessoaOracleType();
         SQLInput input = 
           new SQLInputImpl(row.getAttributes(), row.getMap());
         vot.readSQL(input, row.getSQLTypeName());
         Pessoa pessoa = 
           new Pessoa(vot.getId(), vot.getIdade(), vot.getNome());
         pessoas.add(pessoa);
      }
      return pessoas;
   }
}

Por fim, implemente um método no DAO para fazer a chamada da procedure.


@Stateless
public class PessoaDao
{
   @PersistenceContext(unitName = "pu")
   private EntityManager em;

   private Session getSession(){
      return (Session) em.getDelegate();
   }

   public List<Pessoa> listar() {
      Procedure procedure = new Procedure(getSession());
      procedure.execute();
      return procedure.getPessoas();
   }
}

Conclusão

Lidar com estruturas de banco de dados e com esses tipos do Oracle em especial é particularmente difícil para um desenvolvedor de software, mas tenha em mente que esses problemas se apresentam de vez em quando e você, como engenheiro de software e não um mero desenvolverdor, deve encontrar a melhor solução dadas as restrições do projeto como as que discuti aqui.

Referências

1. [https://docs.oracle.com/cd/B19306_01/appdev.102/b14261/objects.htm]
2. [https://docs.oracle.com/cd/B28359_01/appdev.111/b28371/adobjbas.htm]
3. [https://docs.oracle.com/javase/7/docs/api/java/sql/SQLData.html]
4. [https://docs.oracle.com/javase/tutorial/jdbc/basics/sqlcustommapping.html]
5. [https://docs.oracle.com/javase/7/docs/api/java/sql/SQLInput.html]
6. [https://docs.oracle.com/cd/A87860_01/doc/java.817/a83724/oraoot3.htm]
7. [https://docs.oracle.com/cd/A87860_01/doc/java.817/a83724/oraoot1.htm]
8. [http://docstore.mik.ua/orelly/oracle/prog2/ch18_02.htm]
9. [http://blog.mclaughlinsoftware.com/2012/02/14/how-to-use-object-types/]
10. [https://docs.oracle.com/cd/B13789_01/appdev.101/b10807/10_objs.htm]
11. [https://docs.oracle.com/cd/B28359_01/appdev.111/b28371/adobjplsql.htm#i7530]
12. [https://docs.oracle.com/cd/B19306_01/appdev.102/b14261/objects.htm#i20461]

Mapeamento de Stored Procedures do Oracle em Java

Uma procedure é um conjunto de declarações PL/SQL que pode ser chamada por um nome e pode aceitar argumentos (parâmetros). Como uma procedure é armazenada como um schema no banco de dados, o código pode ser reutilizado. Essas características tornam uma procedure mais eficiente que um bloco anônimo.

Como adiantei nesse artigo, procedures podem retornar valor. Vamos fazer uma procedure que, dado o identificador de uma pessoa, retorna o próprio identificador, a idade e o nome.

1. Crie uma tabela para armazenar os dados das pessoas:

CREATE TABLE TBL_PESSOA
(
  PK     NUMBER(3) NOT NULL,
  NOME   VARCHAR2(64) NOT NULL,
  IDADE  NUMBER(2) NOT NULL
);

2. Vamos inserir um registro para poder validar a procedure:

INSERT INTO TBL_PESSOA
VALUES (1,'JOSÉ', 30);

3. Defina a procedure. Note que a implementação da nossa procedure tem apenas um select simples restrito ao parâmetro de entrada e faz a atribuição de valores aos parâmetros de saída:

CREATE OR REPLACE PROCEDURE PROCEDURE_BUSCAR_USUARIO
  (P_IN_ID IN NUMBER, P_ID OUT NUMBER, 
   P_IDADE OUT NUMBER, P_NOME OUT VARCHAR2)
IS
PK NUMBER;
NOME VARCHAR2(64);
IDADE NUMBER;
BEGIN
   SELECT TP.PK, TP.NOME, TP.IDADE
   INTO PK, NOME, IDADE
   FROM TBL_PESSOA TP
   WHERE TP.PK = P_IN_ID;
   P_ID := PK;
   P_NOME := NOME;
   P_IDADE := IDADE;
END PROCEDURE_BUSCAR_USUARIO;

4. Vamos definir a classe Pessoa. Como não estamos trabalhando com ORM, não é necessário anotações da JPA. Basta um POJO:

public class Pessoa {
   private Integer id;
   private String nome;
   private Integer idade;
   // getters e setters
   public Pessoa(Integer id, Integer idade, String nome) {
      this.id = id;
      this.idade = idade;
      this.nome = nome;
   }
}

5. Vamos utilizar a interface Work do Hibernate para ter acesso à Connection:

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
import org.hibernate.Session;
import org.hibernate.jdbc.Work;

public class Procedure {
   private Pessoa pessoa;
   private Session session;
   public Procedure(Session session) {
      this.session = session;
   }
   public Pessoa getPessoa() {
      return pessoa;
   }
   public void execute(final Integer idPessoa) {
      session.doWork(new Work() {
         @Override
         public void execute(Connection con) throws SQLException {
            CallableStatement callStm = null;
            try {
               callStm = con.prepareCall(
                "{call PROCEDURE_BUSCAR_USUARIO(?, ?, ?, ?)}");
               registrarParametroEntrada(callStm, "P_IN_ID", idPessoa);
               registrarParametroSaida(callStm, "P_ID", Types.INTEGER);
               registrarParametroSaida(callStm, "P_IDADE", Types.INTEGER);
               registrarParametroSaida(callStm, "P_NOME", Types.VARCHAR);
               callStm.execute();
               Integer id = extrairInteger(callStm, "P_ID");
               Integer idade = extrairInteger(callStm, "P_IDADE");
               String nome = extrairString(callStm, "P_NOME");
               pessoa = new Pessoa(id, idade, nome);
            } finally {
               if (callStm != null) {
                  callStm.close();
               }
            }
         }
      });
   }

   private <T extends Object> T extrair(CallableStatement cs,  
        String nomeParametro, Class<T> clazz)
      throws SQLException {
      Object object = cs.getObject(nomeParametro);
      if (object == null) {
         return null;
      }
      return clazz.cast(object);
   }
   
   private String extrairString(CallableStatement cs, 
        String nomeParametro) throws SQLException {
      return extrair(cs, nomeParametro, String.class);
   }

   private Integer extrairInteger(CallableStatement cs, 
        String nomeParametro) throws SQLException {
      return extrair(cs, nomeParametro, Integer.class);
   }

   private void registrarParametroEntrada(CallableStatement cs, 
        String nomeParametro, Integer valor)
      throws SQLException {
      cs.setObject(nomeParametro, valor, Types.INTEGER);
   }

    private void registrarParametroSaida(CallableStatement cs, 
      String nomeParametro, int sqlType) throws SQLException {
      cs.registerOutParameter(nomeParametro, sqlType);
   }
}

6. Por fim, crie um método no seu DAO para acessar a procedure:

@Stateless
public class PessoaDao
{
   @PersistenceContext(unitName = "pu")
   private EntityManager em;

   private Session getSession(){
      return (Session) em.getDelegate();
   }

   public Pessoa buscar(final Integer idPessoa) {
      Procedure procedure = new Procedure(getSession());
      procedure.execute(idPessoa);
      return procedure.getPessoa();
   }
}

Referências

1. [https://docs.oracle.com/cd/B28359_01/appdev.111/b28843/tdddg_procedures.htm]
2. [https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_5009.htm]
3. [https://justonedeveloper.wordpress.com/2013/05/29/convert-hibernate-session-to-jdbc-connection/]