Monday, September 16, 2013

Algoritmos de ordenação String (char) e o uso de Collator no Java 7

Um dos temas que eu mais gosto na área da computação é a Análise de Algoritmos, especificamente a ordenação de estrutura de dados. O Java utiliza diversos algoritmos na classificação de dados em sua API, com o foco em unir a eficiência com a performance. Inclusive, no Java 7 foram implementadas algumas melhorias no uso de algoritmos de ordenação. Nesse post eu descrevo as características sobre ordenação de caracteres e String em Java.

Arrays de char

Pra começar, um exemplo simples demonstrando como ordenar os caracteres que compõe uma String:
  String content = "ebdca";
  char[] chars = content.toCharArray();
  
  Arrays.sort(chars);
  
  String sorted = new String(chars);
  System.out.println(sorted); //abcde

A classe Arrays do Java ordena o array chars na ordem natural, alfabética. Mas o que acontece dentro do método sort? Como o array é pequeno o algoritmo utilizado para ordenação é o Insertion Sort, aonde a comparação ocorre no sentido de elementos da esquerda para a direita, garantindo que todos os elementos à esquerda estejam ordenados.

Simulação:

Array original -> e b d c a
Comparação 1 -> b < e, sim  -> Resultado: b e d c a
Comparação 2 -> de, sim  -> Resultado: b d e c a
Comparação 3 -> d < b, não
Comparação 4 -> ce, sim  -> Resultado: b d c e a
Comparação 5 -> c < d, sim  -> Resultado: b c d e a
Comparação 6 -> c < b, não
...

A análise matemática de Insertion Sort varia de acordo com o conteúdo do array. No melhor caso, quando o array já está ordenado, o número de comparações seria linear O(n). Enquanto no pior caso, se os elementos estiverem em ordem decrescente, o número de comparações seria quadrático O( N^2). Em arrays pequenos Insertion Sort é uma ótima opção.

Em um array maior, com mais de 47 caracteres, o algoritmo utilizado é o QuickSort c/ Dual-Pivot. Como o nome diz, trata-se de uma variação de QuickSort com dois pivôs, abordagem parecida com QuickSort 3-way. Alguns estudos apontam que essa estratégia pode reduzir em até 10% o tempo em relação ao QuickSort tradicional. O algoritmo é recente, reformulado em 2009 por Vladimir Yaroslavskiy. O uso desses algoritmos fazem parte de melhorias do Java 7, verifique a classe DualPivotQuicksort.java.

Um detalhe muito importante sobre a comparação de char, é que o Java utiliza o Unicode para determinar a pontução de cada caracter. Nesse caso 'A' vem bem antes de 'a', e 'ã' vai bem depois de 'a'. Modificando o conteúdo da variável content para "abcdeãA", o valor de sorted seria "Aabcdeã". Volto a falar sobre isso no decorrer do post.

Array de String

O próximo trecho de código também é bem simples, nele coloco quatro strings em um array e peço para a classes Arrays realizar a ordenação:
  String content[] = { "Jose", "Andre", "Claudia", "Bruna" };
  
  Arrays.sort(content);
  for (String s: content) {
    System.out.print(s+" ");
  }

Por se tratar de um array pequeno, a API do Java utiliza o Binary Insertion Sort, uma alternativa um pouco mais performática que usa a busca binária (binary search) para determinar a posição correta da troca. No pior caso o número de comparações é linearítmica O(n log n).

Já em arrays maiores, a partir de 32 elementos, o algoritmo de ordenação é o TimSort. TimSort é um algoritmo derivado de Merge Sort e Insertion Sort, no melhor caso o número de comparações é linear O(n), no pior é linearítmico O(n log n). Esse também é um algoritmo recente, descoberto em 2002 por Tim Peters, para a linguagem Python. O uso desses algoritmos também fazem parte de melhorias do Java 7, verifique a classes  TimSort.java e ComparableTimSort.java.

Importante a ressalva de quem define a classificação de cada objeto durante o sort é o método compareTo de Comparable.

Coleções de String

No próximo exemplo uso a coleção TreeSet para organizar algumas Strings:
  Collection<String> content = new TreeSet<>();
  content.add("Jose");
  content.add("Andre");
  content.add("Claudia");
  content.add("Bruna");

O TreeSet mantém os elementos classificados via Comparator ou Comparable, nesse caso através do método compareTo de Comparable. O TreeSet, como em versões anteriores do Java, mantém os elementos em TreeMap, que por sua vez continua utilizando o Red-Black Tree como algoritmo de ordenação. Por manter os elementos em uma ãrvore balanceada, esse algoritmo é extremamente eficiente, suas as operações são logaritmicas O(log n).

O trecho a seguir, um exemplo de ordenação em uma lista de String:
  List<String> content = new ArrayList<>();
  content.add("Jose");
  content.add("Andre");
  content.add("Claudia");
  content.add("Bruna");
  Collections.sort(content);

Em listas o Java usa o mesmo mecanismo de ordenação aplicado em arrays. O método sort de Collections usa o toArray da lista p/ recuperar os elementos contidos em um array, realiza a ordenação do array (Arrays.sort) e por fim atualiza os elementos na lista de acordo com resultado da ordenação.

O compareTo não funciona como o esperado?

A classificação natural da String é a ordem alfabética (lexicographically), ela ocorre através do método compareTo de Comparable. Esse método funciona perfeitamente quando não há necessidade de usar caracteres especiais, ou diferenciar letras minúsculas e maiúsculas. No começo do post, citei algumas dificuldades com o uso de caracteres com acentuação e maiúsculas com char, o mesmo vale para a String!

Veja um exemplo:
  String content[] = { "Andrei", "Andréa", "Cláudia", "Claudio" };
  Arrays.sort(content);

O resultado desse sort é: "Andrei", "Andréa", "Claudio" e "Cláudia".

Uma solução simples para esse tipo de problema é usar classe Collator, introduzida a partir do Java 5. Collator é uma classe abstrata, que implementa Comparator e define um conjunto de regras para realizar a comparação entre strings a partir de um determinado locale. Os principais idiomas são suportados por Collator, através de sua classe filha RuleBasedCollator. Uma sobrecarga do método sort, de Arrays, recebe um Comparator como argumento viabilizando o uso de Collator.
Veja:
  String content[] = { "Andrei", "andréa", "Cláudia", "Claudio" };
  Collator colPtBr = Collator.getInstance(new Locale("pt_BR"));
  Arrays.sort(content, colPtBr);

Note o valor "andréa", usando somente letras minúsculas. Agora o resultado do sort é: "andréa", "Andrei", "Cláudia" e "Claudio".

O Collator em pt_BR determina que caso duas palavras sejam semelhantes, variando apenas a acentuação, a palavra com acento vem logo após a palavra sem acento. Veja o resultado de comparações via Collator e Comparable com algumas Strings:
  System.out.println(colPtBr.compare("Andréa", "andrea"));  // 1: Andréa após andrea
  System.out.println("Andréa".compareTo("andrea"));  // -32: Andréa antes andrea

  System.out.println(colPtBr.compare("maca", "maçã"));  // -1: maca antes de maçã
  System.out.println("maca".compareTo("maçã"));  // -132: maca antes de maçã 

Mais um detalhe sobre o Collator pt_BR, é em relação a letras minúsculas e maiúsculas. No caso de duas strings semelhantes, variando apenas letras minúsculas e maiúsculas, o Collator assume que a String com letra minúscula fica antes da String com maiúscula. Veja:
  System.out.println(colPtBr.compare("pedro", "PEDRO")); // -1: pedro antes de PEDRO
  System.out.println("pedro".compareTo("PEDRO")); // 32: pedro após PEDRO

A classe Collator permite o uso de estragégias "mais fortes" de comparação. No método setStrength é possível, por exemplo, pedir ao Collator que as diferenças entre acentuação, maiúsculas e minúsculas sejam ignoradas. Veja um exemplo:
  colPtBr.setStrength(Collator.PRIMARY);
  
  System.out.println(colPtBr.compare("pedro", "PEDRO")); // 0
  System.out.println(colPtBr.compare("Andréa", "andrea"));  // 0
  System.out.println(colPtBr.compare("maca", "maçã"));  // 0

Em casos mais simples, cuja a ordenação deve apenas desconsiderar diferenças entre letras maísculas e minúsculas, é possível usar um Comparator pronto e disponível na classe String: a constante CASE_INSENSITIVE_ORDER.
  String content[] = { "A", "x", "c", "B", "d", "a" };
  Arrays.sort(content, String.CASE_INSENSITIVE_ORDER);

Resultado da ordenação é: A a B c d x

Collator vs performance

O uso em de Collator em grande volume de objetos pode degradar a performance. Uma alternativa para reverter esse problema é utilizar a classe CollationKey, que armazena a chave binária em representação a uma string. Veja como funcionaria essa estratégia:
  String content[] = { "Andrei", "andréa", "Cláudia", "Claudio" };
  CollationKey[] collKeys = new CollationKey[content.length];
  
  Collator colPtBr = Collator.getInstance(new Locale("pt_BR"));
  for (int i = 0; i < collKeys.length; i++) {
    //guarda a chave da string
    collKeys[i] = colPtBr.getCollationKey(content[i]);
  }
  
  Arrays.sort(collKeys); //ordeno as chaves do collator
  
  for (int i=0; i < collKeys.length; i++) {
    System.out.print(content[i]+" ");
  }

O truque desse último trecho é colocar as chaves geradas pelo Collator em um array, ordenar esse array e por fim exibir o as strings de acordo com a ordem das chaves.

Coloquei boa parte do código desse post no github.

@edermag

Wednesday, September 04, 2013

Como extrair um diretório de um Zip c/ Java

Exemplo de um programa simples, com Java 7, para extrair um diretório (e filhos se existir) contido em um arquivo zip (tar / war / ear / jar ...).
public class ExtractZipDir {
 
  public static void main(String[] args ) {
    if (args.length < 2) {
      System.out.println("Informe o arquivo zip e a pasta");
      return;
    }
    
    String zipName = args[0];
    String dirName =  args[1];
    extract(zipName, dirName);
  }
  
  static void extract(String zipName, String dirName) {
    long before = currentTimeMillis();
    try (ZipFile zip = new ZipFile(new File(zipName))) {
      ZipContent zContent = new ZipContent(zip);
      int files = zContent.extract(dirName);
      System.out.printf("Extraiu %s arquivos (e, %sms)", files,
        currentTimeMillis() - before);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
    
  private static class ZipContent {
    private final ZipFile zip;
    private Map<String, List<ZipEntry>> content = new HashMap<>();
    private int count;
    
    private static final String SEPARATOR = "/";
    private static final String ROOT_DIRECTORY = ".";
    private static final int BUFFER = 2048;
    
    private ZipContent (ZipFile zipFile) {
      zip = zipFile;
      organizeEntries(zip.entries());
    }
      
    private void organizeEntries(Enumeration<? extends ZipEntry> entries) {
      putRootDir();
       
      while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        putEntry(entry);
      }
    }
      
    private void putEntry(ZipEntry entry) {
      String path = extractDirNameFromEntry(entry);
      List<ZipEntry> entries = content.get(path);
      if (entries == null) {
        content.put(path, new LinkedList<ZipEntry>());
      } else {
        entries.add(entry);
      }
    }
    
    private void putRootDir() {
      content.put(ROOT_DIRECTORY, new LinkedList<ZipEntry>());
    }
    
    private List<ZipEntry> listContent(String folder) {
      return content.get(folder);
    }
    
    private boolean hasDirectory(String folder) {
      return content.keySet().contains(folder);
    }
    
    private Queue<String> getAllDirectoriesFromParent(String dir) {
      Queue<String> q = new LinkedList<>();
      for (String d: content.keySet()) {
        if (d.contains(dir) && !d.equals(dir))
          q.add(d);
      }
      q.add(dir);
      return q;
    }
      
    private int extract(String dir) {
      if (!dir.equals(ROOT_DIRECTORY)) {
        if (!dir.endsWith(SEPARATOR)) {
          dir = dir.concat(SEPARATOR);
        }
        
        if (!hasDirectory(dir)) {
          throw new RuntimeException("Directory not found!");
        }
      }
      
      count = 0;
      Queue<String> dirs = getAllDirectoriesFromParent(dir);
      while (!dirs.isEmpty()) {
        String path = dirs.poll();
        List<ZipEntry> entries = listContent(path);
        File parent = new File(path);
        parent.mkdirs();
        
        for (ZipEntry entry: entries) {
          extract(parent, entry);
          count++;
        }
      }
      return count;
    }
      
    private void extract(File path, ZipEntry e) {
      String fileName = extractFilenameFromEntry(e);
      File destFile = new File(path, fileName);
      try (BufferedInputStream is = new BufferedInputStream(zip.getInputStream(e));
           FileOutputStream fos = new FileOutputStream(destFile);
           BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER)) {
        int currentByte;
        byte data[] = new byte[BUFFER];
        
        while ((currentByte = is.read(data, 0, BUFFER)) != -1) {
          dest.write(data, 0, currentByte);
        }
        dest.flush();
      } catch (IOException e) {
        throw new RuntimeException("Não foi possível extrair!\n" +
          e.getMessage(), e);
      }
    }
    
    private static String extractDirNameFromEntry(ZipEntry entry) {
      String name = entry.getName();
      if (name.lastIndexOf(SEPARATOR) != -1) {
        return name.substring(0, name.lastIndexOf(SEPARATOR)+1);
      }
      return ROOT_DIRECTORY;
    }
    
    private static String extractFilenameFromEntry(ZipEntry entry) {
      String name = entry.getName();
      if (name.lastIndexOf(SEPARATOR) == -1) {
        return name;
      }
      return name.substring(name.lastIndexOf(SEPARATOR)+1);
    }
  }
}

Alguns formas de como executar esse programa (Linux / Windows):
$ java ExtractZipDir /home/user/meu.zip dir
$ java C:\usuario\Documents\meu.zip dir

Compartilhei esse código no github.

@edermag

Thursday, August 15, 2013

Problemas c/ jar assinado pós upgrade do Java 7: invalid SHA1 signature file digest

Durante a migração de um projeto, ou melhor atualização, do Java 6 para o 7, encontrei alguns problemas relacionados certificação dos jars. Um módulo desse projeto é em Swing, instalado no cliente via Java Web Start. A aplicação swing é formada por vários jars, que por sua vez devem ser assinados com o certificado definido pela empresa. Depois de atualizar o workspace e o build, na inicialização da aplicação o Web Start lançou uma java.io.IOException: invalid SHA1 signature file digest for...

O jarsigner mudou no Java 7, o algoritmo default da assinatura passou a ser o SHA-256. No Java 6 era o SHA-1. Esse foi o motivo do erro, na verdade existiam alguns jars usando as duas assinaturas, uma para SHA1 e outra SHA256 (é possível visualizar isso no Manifest.MF).

A solução do problema foi deixar explicito no jarsigner, o uso do algoritmo correto, através da propriedade -digestalg SHA1. Veja:
$ jarsigner -keystore .... -digestalg SHA1

No ANT, é possível definir a propriedade na task signjar:
<signjar ... digestalg="SHA1">
  ...
</signjar>

E por fim, como indicar essa propriedade no Maven:
  <!-- omiti o restante do pom.xml -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jarsigner-plugin</artifactId>
        <version>1.2</version>
        <executions>
          <execution>
            <id>sign</id>
            <phase>package</phase>
            <goals>
              <goal>sign</goal>
            </goals>
          </execution>
        </executions>
 
        <configuration>
          <keystore>...</keystore>
          <alias>...</alias>
          <storepass>...</storepass>
          <keypass>...</keypass>
    <digestalg>SHA1</digestalg>
        </configuration>
      </plugin>
    </plugins>
  </build>
  ...

Em outro post demonstro como usar o jarsigner pelo Maven.

@edermag

Thursday, August 08, 2013

Como executar testes no JMeter (remoto) a partir do ANT

Dica útil para projetos aonde os builds são realizados pelo ANT. O projeto jmeter-ant-task define uma task customizada do ANT, para executar testes (stress / carga / ...) com o JMeter.

Na task jmeter existem diversos atributos utilizados para indicar qual é o diretório de instalação do JMeter, qual é o diretório com os planos de testes (arquivos jmx), qual diretório deve conter resultados dos testes devem ser armazenados e outros.

O trecho de build.xml a seguir demonstra como utilizar a task para executar o plano de testes MyTest.jmx. 
<project name="MyProject" basedir=".">

  <!-- define uma var que guarda a url de app qualquer -->
  <property name="app.url" value="htpp://localhost:8080/myapp" />

  <!-- vars utilizadas pelo jmeter -->
  <property name="jmeter-home" value="/opt/jmeter" />
  <property name="jmeter-lib-dir" value="${jmeter-home}/lib" />
  <property name="jmeter-results" value="/home/app/jmeter-results" />
  <property name="jmeter-tests" value="/home/app/jmeter-tests" />

  <!-- Carrega a task do JMeter -->
  <taskdef name="jmeter" classpathref="apache.ant.ext.lib.classpath" 
    classname="org.programmerplanet.ant.taskdefs.jmeter.JMeterTask" />
  
  <target name="jmeter.run" depends="" >
    <echo>Acionando o JMeter local p/ os testes de stress</echo>
    <jmeter jmeterhome="${jmeter-home}" testplan="${jmeter-tests}/MyTest.jmx"
     resultlogdir="${jmeter-results}/jtl" jmeterlogfile="${jmeter-results}/log">
     
      <property name="app.url" value="${app.url}"/>

      <!-- passa argumentos p/ a vm do jmeter -->
      <jvmarg value="-Xincgc"/>
      <jvmarg value="-Xmx128m"/>
    </jmeter>
  </target>

  ...
</project>

No inicio do build são definidas variáveis utilizadas pelo o script, boa parte delas na task jmeter. Um detalhe sobre o plano de teste é que ele depende de uma variável que indica a url da aplicação, app.urlNote o uso da tag property, nela eu repasso a variável definida no build ANT, a ${app.url} com o endereço da aplicação. Dessa é possível enviar informações para o JMeter, sem manipular o arquivo jmeter.propertiesNa tag jvmarg você pode indicar parâmetros para a VM do JMeter.

O target a seguir simila a execução do Jmeter remoto, em outro servidor. Para executar o JMeter remoto, é necessário pelo menos dois nós (instâncias) do JMeter, uma cliente e outra servidor, veja mais na documentação do projeto. O próximo target aciona o JMeter cliente, na mesma máquina que o build está sendo executado, indicando a propriedade remote_hosts. Dessa forma a instância cliente irá acionar o JMeter servidor nesse endereço indicado.
...
  <!-- host/ip para o jmeter remoto -->
  <property name="jmeter-host" value="192.168.1.18"/>
  <property name="jmeter-port" value="1099"/>

  <target name="jmeter.run.remote">
    <echo>Acionando o JMeter remoto p/ os testes de stress</echo>
    <jmeter jmeterhome="${jmeter-home}" jmeterlogfile="${jmeter-results}/log" resultlogdir="${jmeter-results}/jtl" runremote="true">
      <!-- outra forma de definir os planos de testes -->
      <testplans dir="test/jmeter/jmx" includes="MyTest.jmx"/>

      <!-- indica que essa property deve ser enviada ao jmeter server -->
      <property name="app.url" remote="true" value="${app.url}"/>

      <!-- indica a url do jmeter server -->
      <property name="remote_hosts" value="${jmeter-host}:${jmeter-port}"/>
    </jmeter>
  </target>
...

Note que a propriedade app.url foi marcada como remote="true", dessa forma o JMeter cliente repassa o valor para o JMeter servidor. Mais informações sobre essa task foram disponibilizadas na página do projeto.

http://twitter.com/edermag
http://www.yaw.com.br

Thursday, August 01, 2013

Overview das ferramentas do Spring para persistência de dados

Durante o desenvolvimento ou manutenção (principalmente) de um projeto de software, seja qual for o estilo, é sempre delicado lidar com os componentes de infra-estrutura responsáveis pela camada de persistência.

Vou descrever alternativas que o Spring Framework oferece para simplificar esse trabalho, em cenários variados, como o foco em base de dados relacional. Ou seja, um overview das ferramentas do Spring para trabalhar com os componentes de persistência.

Código legado, bem legado...

Projetos Java antigos, desenvolvidos a + de 8 anos, normalmente não utilizam uma solução ORM (Mapeamento Objeto Relacional). Nesses projetos é muito comum o uso de bibliotecas "caseiras", escritas in house, para otimizar o uso do JDBC.

O Spring JDBC é um modulo do Spring interessante para esse tipo de cenário. Com ele é possível reduzir consideravelmente o volume de código JDBC. O principal componente do Spring JDBC é o JdbcTemplate, ele disponibiliza uma série de métodos para operações CRUD, consultas e comandos em lote. Para tirar proveito do uso contextos, injeção de dependências e inversão de controle, faz todo o sentido trabalhar em conjunto com o Spring Bean. Dessa forma seria possível injetar a referência do JdbcTemplate nos componente DAO (pattern Data Access Object).

O código a seguir demonstra um fragmento do DAO que utiliza o JdbcTemplate para inserir/atualizar uma entidade (Mercadoria).
@Component
public class MercadoriaDAO {

  //SQL
  private final static String INSERT_MERCADORIA = "INSERT INTO mercadoria (nome,descricao,preco,quantidade) VALUES (?,?,?,?)";
  private final static String UPDATE_MERCADORIA = "UPDATE mercadoria SET nome = ?, descricao = ?, preco = ?, quantidade = ? WHERE id = ?";
  private final static String GET_MERCADORIA_BY_ID = "SELECT * FROM mercadoria WHERE id = ?";
  private final static String GET_MERCADORIAS_BY_NOME = "SELECT * FROM mercadoria WHERE nome like ?";
  
  @Autowired
  private JdbcTemplate jdbcTemplate;
  
  public void save(Mercadoria m) {
    if (m.getId() == null) {
      jdbcTemplate.update(INSERT_MERCADORIA, 
        new Object[]{ m.getNome(), m.getDescricao(), m.getPreco() });
    } else {
      jdbcTemplate.update(UPDATE_MERCADORIA,
        new Object[]{ m.getNome(), m.getDescricao(), m.getPreco(), m.getId() });
    }
  }
  ...
}

Além do método update também pode ser utilizado para realizar a remoção da entidade. No trecho de código a seguir coloco dois exemplos de consultas, utilizando JdbcTemplate. Note que na consulta utilizamos o componente RowMapper, o MercadoriaRowMapper. O RowMapper é utilizado pelos métodos query de JdbcTemplate, ele lê os dados do ResultSet e faz a transformação em uma instância de Mercadoria.

O método queryForObject é utilizado para retornar uma instância da entidade (ou null) com filtro por id, por exemplo. Enquanto o método query retorna uma lista de objetos que podem ser encontrados de acordo com o SQL.
  ... //ainda em MercadoriaDAO
  
  private class MercadoriaRowMapper implements RowMapper<Mercadoria> {

    public Mercadoria mapRow(ResultSet rs, int row) throws SQLException {
      int id = rs.getInt("id");
      String nome = rs.getString("nome");
      String descricao = rs.getString("descricao");
      double preco = rs.getDouble("preco");
   
      return new Mercadoria(id, nome, descricao, qtde, preco);
    }
  }

  public Mercadoria findById(Integer id) {
    return jdbcTemplate.queryForObject(GET_MERCADORIA_BY_ID,
      new Object[] { id }, new MercadoriaRowMapper());
  }
  
  public List<Mercadoria> getMercadoriasByNome(String nome) {
    return jdbcTemplate.query(GET_MERCADORIAS_BY_NOME,
      new Object[] { nome + "%" }, new MercadoriaRowMapper());
  }
  ...

Outra estratégia, que não me agrada, seria fazer no DAO a Mercadoria uma herança para JdbcDaoSupport, e acessar o JdbcTemplate encapsulado nesse componente. Com Spring JDBC o código DAO pode ficar bem mais compacto. Para ilustrar isso, compare o DAO c/ JDBC puro e o DAO utilizando Spring JDBC. Veja também as configurações do Spring e o o pom.xml com as dependências para esses módulos.

Projetos com Hibernate

O Spring também oferece soluções para reduzir o esforço e agregar funcionalidades durante o desenvolvimento de projetos Java utilizando soluções ORM, como Hibernate ou JPA. O Spring ORM é outro módulo da suíte Spring, ele oferece funcionalidades para facilitar o uso de soluções baseadas em Mapeamento Objeto Relacional.

Em versões antigas do Hibernate, era trabalhoso manter a Session vinculada ao contexto de execução, por exemplo utilizar a mesma instância em diferentes DAOs dentro do mesmo fluxo de request. Por isso o Spring criou o HibernateTemplate, com a proposta de disponibilizar a Session corrente ao contexto de execução.

Mas a partir do Hibernate 3.0.1 com contextual sessions, isso não é mais necessário. Nas versões mais recentes do Spring é possível usar a Session no contexto de execução, além de centralizar as configurações e realizar a injeção da SessionFactory nos DAOs.

A seguir o código do DAO da Mercadoria com Hibernate:
@Component
public class MercadoriaDAO {

  @Autowired
  private SessionFactory sessionFactory;
  
  private final Session getCurrentSession(){
    return this.sessionFactory.getCurrentSession();
  }
  
  public void save(Mercadoria m) {
    if (m.getId() == null) {
      this.getCurrentSession().persist(m);
    } else {
      this.getCurrentSession().merge(m);
    }
  }
  
  public Mercadoria findById(Integer id) {
    return (Mercadoria) this.getCurrentSession().get(Mercadoria.class, id);
  }
  
  public List<Mercadoria> getMercadoriasByNome(String nome) {
    return this.getCurrentSession()
      .createQuery("from model.Mercadoria m where m.nome like ?")
      .setParameter(0, nome+"%")
      .list();
  }
  ...
}

O spring-config.xml a seguir centraliza no Spring as configurações com banco de dados e do Hibernate.  Nesse exemplo o banco de dados utilizado é o HSQLDB (local).
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-3.0.xsd">
  
  <context:component-scan base-package="." />
  
  <!-- SessionFactory, DataSource, ... -->

  <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
    <property name="dataSource" ref="ds" />
    <property name="packagesToScan" value="model" />
  
    <property name="hibernateProperties">
      <props>
        <prop key="hibernate.dialect"> org.hibernate.dialect.HSQLDialect</prop>
        <prop key="hibernate.show_sql">true</prop>
        <prop key="hibernate.hbm2ddl.auto">create</prop>
      </props>
    </property>
  </bean>
  
  <bean id="ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
    <property name="url" value="jdbc:hsqldb:file:mercadoria"/>
    <property name="username" value="sa"/>
    <property name="password" value=""/>
  </bean>
  ...
</beans>

Projetos com JPA++

O Spring também oferece funcionalidades bem interessantes para projetos que utilizam a Java Persistence API (JPA), através do módulo Spring Data JPA. Através da interface JpaRepository o Spring define os métodos de consulta e CRUD. O desenvolvedor trabalha de forma alto-nível, criando uma interface de persistência, enquanto a implementação fica por conta do próprio Spring. Outro elemento do Spring Data JPA é a anotação @Query, responsável por informar consultas customizadas via JPAQL.

Para esse tipo de componente o Spring adota o pattern Repository, que abstrai uma coleção de objetos com o design mais próximo ao domínio da aplicação do que com o banco de dados. Veja como ficaria a interface MercadoriaRepository:
public interface MercadoriaRepository extends JpaRepository<Mercadoria, Integer> {

  @Query("select m from Mercadoria m where m.nome like ?1")
  List<Mercadoria> getMercadoriasByNome(String nome);

}

Uma vez que a interface foi definida, o Spring cria um proxy que implementa os métodos de persistência. Esse proxy será injetado em classe de negócio/controller pelo Spring. Veja o exemplo:
@Component
public class MercadoriaService {

  @Autowired
  private MercadoriaRepository repo;
  
  public void save(Mercadoria m) {
    m.save(m);
  }
  ...
}

Nesse exemplo demonstrei funcionalidades básicas da interface JpaRepository, mas existem outrs funcionalidades como o suporte a paginação da consulta SQL, veja esse exemplo. No github compartilhamos outros dois projetos que utilizam Spring Data JPA, uma aplicação web e outra desktop. Ambas fazem uso do JpaRepository.

Um pouco além: NoSQL

Na verdade esse módulo compõe o Spring Data, uma solução "guarda-chuva" com o objetivo de unificar e simplicar o armazenamento de dados em bancos relacionais e NoSQL. Abaixo desse projeto existe o módulo Spring Data MongoDB, responsável por abstrair o acesso ao MongoDB. Não é o proposito desse post abordar soluções NoSQL, mas para ter uma idéia o trecho de código a seguir demonstra repositório da Mercadoria (como Document) em versão MongoDB:
public interface MercadoriaRepository extends MongoRepository<User, String> { 
  
  @Query("{ nome: ?0 }")
  List<User> getMercadoriasByNome(String nome);
  
}

Abaixo do Spring Data ainda existem sub-projetos para Neo4j, Apache Hadoop, REST e outros. É possível saber um pouco mais sobre essa tecnologia, em artigo introdutório sobre Spring Data no InfoQ Brasil.

Esse é apenas um resumo de algumas soluções oferecidas pelo Spring Framework para resolver questões relacionadas a persistência em projetos Java. Sou da turma que gosta do Spring e de Java EE. Acredito que tirando proveito das melhoras funcionalidades das duas stacks, aumentamos o nosso poder fogo e logo a possibilidade de desenvolver um projeto com sucesso.

http://twitter.com/edermag
http://www.yaw.com.br

Saturday, July 20, 2013

Testes integrados com JUnit, Maven e Jetty

Nesse post descrevo uma alternativa para configurar e executar testes unitários e integrados com JUnit através do Maven. O Maven, em seu ciclo de build, define a fase (phase) integration-test. Essa fase realiza os procedimentos do deploy com a possibilidade de executar rotinas de testes após a implantação do artefato.

O contexto que utilizo aqui é de uma aplicação web com serviços REST. Nesse caso seria útil implementar testes de integração para testar o funcionamento desses serviços REST. É possível no mesmo projeto, dentro do diretório src/test/java implementar rotinas de testes unitários e integrados. No Maven podemos segmentar os tipos de testes, na configuração do plugin maven-surefire-plugin. No pom.xml indicamos quais são os testes que devem ser executados após o deploy, na fase integration-test.

A seguir o techo do pom.xml, aonde configuro o plugin, indicando quais são os componentes de teste unitário e quais são de testes integrados.
<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
...
  <build>
    ...

    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.15</version>
        <configuration>
          <excludes>
            <exclude>**/integration/*.java</exclude>
          </excludes>
       </configuration>
       <executions>
         <execution>
            <id>integration-test</id>
            <goals>
              <goal>test</goal>
            </goals> 
            <phase>integration-test</phase>
            <configuration>
              <excludes>
                <exclude>none</exclude>
              </excludes>
              <includes>
                <include>**/integration/*.java</include>
              </includes>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

...
</project>

A estratégia foi criar um pacote com conteúdo integration, e nele armazenar os componentes que implementam os testes de integração. Para executar os testes integrados bastar executar a instrução:
mvn integration-test

O build do Maven valida, compila, faz o pacote, realiza o deploy do pacote e por fim processa os testes integrados. Compartilhei no meu github um projeto, uma aplicação web c/ RESTEasy, que utiliza essa alternativa para processar testes integrados com Maven.

http://twitter.com/edermag
http://www.yaw.com.br

Wednesday, July 17, 2013

Maven e Jetty uma excelente combinação p/ desenv web

Minhas experiências no desenvolvimento de projetos web com Maven e Jetty, até agora, foram positivas. Essa combinação é eficiente e produtiva, favorece o fluxo de trabalho codifica + compila + executa + testa.

Trabalhar com o Jetty, em um projeto gerenciado pelo Maven, é algo bem simples. Basta configurar o pom.xml com o plugin jetty-maven-plugin. O trecho a seguir é um exemplo de como habilitar o plugin no projeto:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  ...

  <build>
    <plugins>
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>8.1.11.v20130520</version>
      </plugin>
    </plugins>
  </build>

</project>

Após configurar o Maven, existem várias formas para trabalhar com o Jetty. Um dos goals mais interessantes é o jetty:run, com ele o Maven roda o projeto sem gerar o war, ou seja, os fontes são implantados "diretamente" no Jetty:
mvn jetty:run

Depois dessa instrução o Jetty é inicializado (por default na porta 8080) e os fontes do projeto são implantados, acessível pela url http://localhost:8080/. É possível encerrar a execução do Jetty com ctrl-c na console, ou em alguma outra aba acionar o goal jetty:stop, veja:
mvn jetty:stop

Essa estratégia é interessante para produtividade durante o desenvolvimento, caso alguma classe seja modificada o plugin do Jetty implanta a mudança, realiza o hot deploy.

Outra alternativa é executar o Jetty e realizar o deploy do war, após a geração do pacote pelo Maven.
mvn jetty:run-war

Essa abordagem faz sentido quando não é necessário realizar hot deploy de mudanças após o deploy no Jetty.

O plugin suporta diversas configurações, como por exemplo a porta de execução do Jetty e tempo para verificar fontes (hot deploy). Veja um exemplo do plugin com configurações complementares:
...
  <plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>8.1.11.v20130520</version>
    <configuration>
      <scanIntervalSeconds>10</scanIntervalSeconds>
      <webApp>
        <!-- Contexto da aplicação -->
        <contextPath>/${project.artifactId}</contextPath>
      </webApp>
      <connectors>
        <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
          <!-- Porta do Jetty -->
          <port>8000</port>
          <maxIdleTime>60000</maxIdleTime>
        </connector>
      </connectors>
    </configuration>
  </plugin>
...

De acordo com a configuração do plugin acima, o Jetty vai rodar na porta 8000 utilizando o nome do artefato como contexto web, sendo que cada conexão com container (Socket) tem um timeout de 60 segundos. Além disso o plugin verifica modificações no código a cada 10 segundos, para realizar o hot deploy.

Compartilhei um projeto Java web no github, que demonstra como funciona a integração do Maven com o Jetty. Saiba mais detalhes sobre o jetty-maven-plugin, na página do plugin.

http://twitter.com/edermag
http://www.yaw.com.br

Wednesday, July 10, 2013

Configurar o Datasource do MySQL no Jetty

O Jetty é um web server e servlet container, com ele é possível servir conteúdo estático e/ou dinâmico (Java EE Web). Trata-se de um projeto open source, atualmente mantido pela fundação Eclipse, uma solução escolhida por muitos desenvolvedores pela simplicidade e eficiência.

No Jetty é possível configurar datasource com o banco de dados de várias formas. Aqui nesse post, vou descrever os passos para realizar a configuração do datasource do MySQL no Jetty, e como acessá-lo via JPA/Hibernate.

Uma boa prática é utilizar um pooling de conexões com o banco de dados na definição de datasources, otimizando o consumo de memória e velocidade de requisições com o banco. Por isso vou utilizar um plugin c3p0 com Jetty na configuração do datasource.

A estratégia é adicionar um novo arquivo de configuração, xml, na pasta WEB-INF do projeto web. O nome do arquivo com a configuração do datasource é jetty-env.xml. Veja o exemplo
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure 
  PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" 
  "http://jetty.mortbay.org/configure.dtd">

<Configure id="wac" class="org.eclipse.jetty.webapp.WebAppContext">

  <!-- MySQL DataSource -->
  <New id="MysqlDS" class="org.eclipse.jetty.plus.jndi.Resource">
    <Arg></Arg>
    <Arg>jdbc/MysqlDS</Arg>
    <Arg>
      <New class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <Set name="driverClass">com.mysql.jdbc.Driver</Set>
        <Set name="jdbcUrl">jdbc:mysql://localhost:3306/db</Set> <!--url jdbc-->
        <Set name="user">root</Set> <!-- usuario do banco de dados -->
        <Set name="password">root</Set> <!-- senha do banco de dados -->
      </New>
    </Arg>
  </New>
</Configure>

Note que que no jetty-env.xml defino o JNDI name jdbc/MySQL, configure as as credenciais e a url jdbc de acordo com sua instalação mySQL. Agora é necessário fazer a busca do datasource pelo JNDI name configurado, a seguir o exemplo de como fazer em isso em JPA, no arquivo persistence.xml (JPA):
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0" 
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="estoqueUnit" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <non-jta-data-source>jdbc/MysqlDS</non-jta-data-source>
    <properties>
      <!-- configurações do Hibernate -->
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
    </properties>
  </persistence-unit>
</persistence>

Um detalhe importante, é necessário garantir os jars do driver JDBC MySQL e c3p0 na distribuição do projeto web (WEB-INF/lib). No Maven basta apenas adicionar as duas dependências no pom.xml, como a seguir:
<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  ...

  <dependencies>
    ...
    <!-- MySQL JDBC -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.25</version>
    </dependency>

    <!-- C3p0 -->
    <dependency>
      <groupId>c3p0</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.1.2</version>
    </dependency>
    ...
  </dependencies>

  ...
</project>

Mais detalhes na documentação do Jetty.

http://twitter.com/edermag
http://www.yaw.com.br

Wednesday, June 12, 2013

MySQL não inicializa após atualização do Ubuntu

Após realizar a atualização do Ubuntu p/ a versão 12.04, o MySQL que estava instalado nessa mesma máquina parou de funcionar. Ao tentar inicializar o serviço o seguinte erro era apresentando start: Job failed to start.

Analisando o log do banco encontrei o seguinte trecho:
init: Failed to spawn mysql main process: unable to execute: No such file or directory

Em um fórum sobre Ubuntu, existe uma thread que envolve o assunto, que apresenta possíveis soluções. No meu caso, para resolver o problema fiz o backup dos dados (schemas / tables / data) e reinstalei os serviços do MySQL no Ubuntu.

Primeiro passo é fazer o backup do MySQL. Na verdade isso é muito simples basta replicar o conteúdo da pasta /var/lib/mysql. A seguir os comandos para realizar isso (note que o nome do meu usuário linux é yaw, utilize o nome do seu usuário):
yaw@local:~/$ mkdir /home/yaw/mysql/
yaw@local:~/$ sudo chmod 777 -R mysql
yaw@local:~/$ cd /var/lib/mysql/
yaw@local:/var/lib/mysql$ sudo cp * /home/yaw/mysql/ -R

Proxima etapa é desinstalar o MySQL no Ubuntu (use a versão do MySQL adequada):
yaw@local:~/$ sudo apt-get purge mysql-server-{version} mysql-common

Agora é o momento de restaurar o backup do MySQL, voltar a pasta mysql para o diretório /var/lib, veja:
yaw@local:~/$ sudo mkdir /var/lib/mysql/
yaw@local:~/$ sudo chown root:root /var/lib/mysql/ -R
yaw@local:~/$ cd ~/mysql
yaw@local:~/$ sudo cp * /var/lib/mysql/ -R
yaw@local:~/$ cd ..
yaw@local:~/$ sudo rm -rf ~/mysql

Por fim a instalação do MySQL no Ubuntu:
yaw@local:~/$ sudo apt-get install mysql-server

Pronto, o MySQL foi "restaurado" no Ubuntu 12.04.

[]s

Thursday, June 06, 2013

Openshift, como copiar o arquivo de log do Tomcat 7

Neste post descrevo os passos para copiar o arquivo de log do Tomcat 7, rodando no Openshift (plataforma cloud da Red Hat). O objetivo de copiar o arquivo de log do Openshift para minha máquina, é simplesmente facilitar a consulta e análise dos registros de log.

Primeiro é necessário acessar a aplicação no Openshift via ssh (a forma mais simples é copiar a hash ssh na página da sua aplicação no site do próprio Openshift).

O Openshift mantém uma série de variáveis de ambiente pré-definidas, com o propósito de armazenar o caminho para algumas pastas do sistema de arquivos da plataforma. Todas essas variáveis possuem o prefixo OPENSHIFT_. Com o comando a seguir é possível listar e visualizar o conteúdo de todas essas variáveis:

<openshift>$ env | grep OPENSHIFT_

Para acessar os logs do Tomcat (JBoss EWS), utilizo a variável de ambiente OPENSHIFT_JBOSSEWS_LOG_DIR. A próxima etapa é fazer a cópia do arquivo de log para o diretório de dados (app-root/data) no OpenShift. A variável de ambiente que aponta para esse diretório é OPENSHIFT_DATA_DIR. A seguir a instrução para realizar a cópia do arquivo catalina.out:

<openshit>$ cp $OPENSHIFT_JBOSSEWS_LOG_DIR/catalina.out $OPENSHIFT_DATA_DIR

Lembre-se que por questões de segurança o Openshift só permite a transferência via ssh de arquivos contidos na pasta data

Agora é só baixar (copiar) o arquivo catalina.out do Openshift para a máquina local:

yaw@local:~/$ scp {hash_}@{app}.rhcloud.com/~/app-root/data/catalina.out /~ 

Pronto, com o arquivo local fica mais fácil analisar o log da aplicação.

[]s

Tuesday, June 04, 2013

Nodejs: configuração de proxy c/ autenticação no npm

Dica útil para resolver problemas com npm, o gerenciador de pacotes do Nodejs. O erro "npm error Error: tunneling socket could not be established", pode ocorrer devido ao uso de proxy com autenticação.

No Ubuntu, o npm respeita a configuração de proxy (http e https) das variáveis de ambiente http_proxy e https_proxy, respectivamente. Para visualizar as configurações de proxy do npm, utilize o seguinte comando:

$ npm config get proxy

O comando deve exibir o host (ip) do proxy configurado. Pra informar as credenciais do proxy ao npm, é necessário modificar a configuração do gerenciador, isso pode ser feito com o comando (utilize suas informações de crendencial):

$ npm config set proxy http://usuario:senha@host_proxy:porta_proxy

Para https, utilize a property https-proxy nas configurações do npm.

Agora o npm install vai realizar a autenticação no proxy.

[]s
http://twitter.com/edermag
http://www.yaw.com.br

Monday, May 27, 2013

RESTEasy: Erro ao retornar List em XML - Could not find MessageBodyWriter for response object of type: xxx application/xml

Outro dia me deparei com uma exception gerada pelo RESTEasy, o erro era: Could not find MessageBodyWriter for response object of type: java.util.ArrayList of media type: application/xml.

No meu cenário, o problema ocorria ao utilizar o componente Response do RESTEasy como retorno do método. Pra ficar mais claro a seguir um exemplo de código simulando o erro, no caso a entidade e o endpoint do RESTEasy:
@Entity
@XmlRootElement
public class Carro {

  @Id
  private Long id;

  @NotNull
  private String renavam;

  private String placa;

  @NotNull
  @ManyToOne
  private Modelo modelo;

  //...

}


@Path("/carros")
public class CarroEndpoint {
  ...

  @GET
  @Path("/{id:[0-9][0-9]*}")
  @Produces("application/xml")
  public Response findById(@PathParam("id") Long id) {
    try {
      Carro c = service.findById(id); 
      return Response.ok(c).build();
    } catch (NoResultException nrex) {
      return Response.status(Status.NOT_FOUND).build(); //404
    }
  }

  @GET
  @Produces("application/xml")
  public List<Carro> listAll() {
    return service.listAll();
  }

  @GET
  @Path("/{modelo}")
  @Produces("application/xml")
  public Response listByModelo(@PathParam("modelo") String descModelo) {
    Modelo m = service.findModeloByDescricao(descModelo);
    if (m == null) {
      return Response.status(Status.NOT_FOUND).build();
    }

    List<Carro> carros = service.findByModelo(m);
    return Response.ok(carros).build(); //o problema ocorre aqui
  }

}

Os  métodos findById e listAll funcionam corretamente, ambos devolvem o XML representando os dados dos carros encontrados de acordo com a consulta. Note o retorno desses dois métodos: a entidade Carro e uma lista de Carros.

A exception ocorre no terceiro método, no listByModelo, aonde faço uso da Response (componente do RESTEasy). Nesse caso o RESTEasy não consegue transformar o ArrayList em XML. A mesma situação acontece com outra coleção, por exemplo HashSet.

Pra resolver o problema, eu utilizei o GenericEntity para "tipar" a lista, de forma que o RESTEasy possa realizar a transformação em XML. A mesma abordagem funciona com HashSet. A seguir a nova versão de listByModelo:
  ...

  @GET
  @Path("/{modelo}")
  @Produces("application/xml")
  public Response listByModelo(@PathParam("modelo") String descModelo) {
    Modelo m = service.findModeloByDescricao(descModelo);
    if (m == null) {
      return Response.status(Status.NOT_FOUND).build();
    }

    List<Carro> carros = service.findByModelo(m);
    GenericEntity<List<Carro>> entity = new GenericEntity<List<Carro>>(carros);
    return Response.ok(entity).build(); //ok
  }
  ...

A versão do RESTEasy utilizada foi a 2.3.6.Final.

[]s
http://twitter.com/edermag
http://www.yaw.com.br

Wednesday, May 15, 2013

Configurar conexões remotas para o MongoDB no Ubuntu

Uma vez que o mongoDB já foi instalado no Ubuntu, é muito simples habilitar conexões remotas para o serviço. Basta indicar o uso do ip e porta no arquivo de configurações do mongoDB, mongodb.conf. (Acesse c/ super-usuário - sudo)

$ sudo gedit /etc/mongodb.conf

Inclua esses dois parametros:
bind_ip = 0.0.0.0
port = 27017

A propriedade port já vem comentada na instalação padrão.

Importante: nesse post não levo em consideração aspectos de segurança e restrição de acesso ao mongoDB.

Você pode acessar o mongoDB de outra máquina, indicando o ip dá maquina aonde o banco foi instalado. Esse acesso pode ocorrer via terminal ou com um ferramenta GUI para o MongoDB, como o UMongo (app p/ Linux).

É isso!

http://twitter.com/edermag
http://www.yaw.com.br

Monday, May 06, 2013

Openshift, como fazer o deploy de um war pronto no Tomcat

No Openshift é possível trabalhar com diversas tecnologias, como: Java, PHP, Ruby, Node.js e até Perl. Além das linguagens, a plataforma cloud computing da Red Hat (PaaS) também oferece suporte a uma série de serviços complementares, como gestor de build (Maven p/ Java), ferramenta para integração contínua (Jenkins), controlador e repositório de fontes (Git).

O Openshift agrega esses serviços/ferramentas a plataforma com objetivo de ampliar a capacidade de desenvolvimento. Dessa forma além de usar o hosting em nuvem, você pode contar com uma melhor gestão de build, baseado na execução de testes, com a possibilidade de commits em repositórios segmentados.

Mas é possível ignorar esses serviços, e fazer o deploy da aplicação "pronta" no Openshift. Pronto no sentindo de que o artefato foi gerado local, sem interferência do Openshift. Isso pode ser interessante em situações pontuais, como demos ou provas de conceito.

Nesse post demonstro como realizar o deploy de uma aplicação Java web (um war), no Tomcat, sem utilizar o Maven, Jenkins e Git*.
*Na verdade não colocamos os fontes no Git, mas o utilizamos para armazenar o war da aplicação.

1 - Crie a aplicação no Openshift, e faça o clone local do repositório Git. Note que dentro do diretório principal, no repositório local, existe a subpasta webapps.

2 - Acesse o código fonte da aplicação para gerar o artefato web, o war. Se preferir utilize o Maven, rodando local, para controlar o build gerar o war.

3 - Copie o arquivo war para a pasta webapps, dentro do repositório Git local:
$ cp aplicacao.war  [caminho do repositorio]/webapps/

4 - Coloque o arquivo no repositório local e master. Execute o fluxo add / commit e push do Git:
$ cd  [caminho do repositorio]/webapps/

$ git add aplicacao.war
$ git commit -m "seu comentario"
$ git push origin master

Em caso de um redeploy, uma nova versão, antes do passo 3, remova o arquivo war atual:
$ git rm aplicacao.war 

Pronto, com esses passos é possível publicar uma aplicação "pronta" no Openshift.

http://twitter.com/edermag
http://www.yaw.com.br

Monday, April 22, 2013

Openshift, como configurar o DataSource para o MySQL no Tomcat 7

Nesse post descrevo como configurar o componente DataSource para o banco de dados MySQL, no Tomcat 7 dentro do OpenShift. O OpenShift é a plataforma de cloud computing da Red Hat, um serviço na modalidade PaaS (Plataform as a Service), que disponibiliza uma infra-estrutura pré-definida para implantação de projetos em nuvem. No OpenShift é possível implantar projetos desenvolvidos em Java, Python, Ruby, JavaScript, PHP ou Perl.

No contexto desse post, configuramos no OpenShift uma aplicação web com uma instância (cartridge) de Tomcat 7 (JBoss EWS) e MySQL 5.1. Com o uso de DataSource, o Tomcat é o responsável por gerenciar (abrir / manter / encerrar) as conexões com o MySQL. A nossa aplicação Java solicita (JNDI) ao Tomcat a referência para o DataSource para acessar os dados.

Depois de criar a aplicação no Openshift e instalar a chave ssh, faça o clone do repositório git em sua máquina (local).

yaw@local/~$ git clone ssh:{hash_}@{app-namespace}.rhcloud.com/~/git/{app}.git

A url git do repositório fica disponível na página do projeto (My Apps no OpenShift).

Definir o DataSource

No repositório local além dos fontes, nós temos acesso a alguns arquivos de configurações dos serviços disponíveis do Openshift. Vamos utilizar alguns desses arquivos para definir do DataSource no Tomcat.

Depois de baixar o repositório (git), entre na pasta do projeto que foi clonado e acesse o subdiretório .openshift:
yaw@local/~$ cd app
yaw@local/~/app$ cd .openshift/

Agora acesse o subdiretório config, local aonde ficam os arquivos de configuração do Tomcat, e modifique o conteúdo do arquivo server.xml:
yaw@local/~/app/.openshift$ cd config
yaw@local/~/app/.openshift/config$ gedit server.xml

No arquivo server.xml é necessário definir a tag Resource com as propriedades do DataSource. Atenção: escreva essa tag dentro da tag GlobalNamingResources, e não remova nenhum conteúdo desse arquivo!

A seguir o trecho do server.xml com a definição da tag Resource (boa parte do conteúdo desse arquivo foi omitido):
<?xml version='1.0' encoding='utf-8'?>
<Server port="-1" shutdown="SHUTDOWN">
  ...

  <GlobalNamingResources>
     ...
    <!-- mysql datasource definition -->
    <Resource auth="Container" driverClassName="com.mysql.jdbc.Driver" type="javax.sql.DataSource" 
      maxActive="20" maxIdle="10" maxWait="-1" name="jdbc/MysqlDS" 
      username="{database_username}" password="{database_password}" 
      url="jdbc:mysql://${env.OPENSHIFT_MYSQL_DB_HOST}:${env.OPENSHIFT_MYSQL_DB_PORT}/{database}" />

  </GlobalNamingResources>

  ...
</Server>

Importante: substitua os valores {database_username}, {database_password} e {database} para os respectivos valores de seu serviço MySQL. Note que utilizei na propriedade name o valor padrão "jdbc/MysqlDS". Esse valor será utilizado posteriormente, na configuração de persistência da aplicação (persistence.xml).

Agora modifique o arquivo context.xml, no mesmo diretório, para definir o link com o DataSource que acabamos de definir. Utilize a tag ResourceLink, conforme o exemplo a seguir:
<?xml version='1.0' encoding='utf-8'?>
<Context>

  <!-- Default set of monitored resources -->
  <WatchedResource>WEB-INF/web.xml</WatchedResource>

  <!-- link for mysql datasource -->
  <ResourceLink name="jdbc/MysqlDS" global="jdbc/MysqlDS" type="javax.sql.DataSource"/>

  ...
</Context>

Instalar o driver JDBC

Agora precisamos instalar no Tomcat o jar do driver MySQL JDBC. O OpenShift mantém restrições de segurança em boa parte da estrutura de arquivos da plataforma. Não é possível copiar um arquivo para a pasta lib do Tomcat. O jar deve ser instalado na pasta app-root/data/ do Openshift. Você pode acessar o OpenShift via ssh e fazer um wget do jar, ou então copiar o arquivo em uma pasta acessível no OpenShift.

A seguir o comando scp para copiar o arquivo da máquina local para o OpenShift (assumindo que o arquivo mysql-connector-java.jar está armazenado em /home/yaw/):
yaw@local/~/app/.openshift/config$ scp /home/yaw/mysql-connector-java.jar
   {hash_}@{app-namespace}.rhcloud.com/~/app-root/data/

Agora é necessário configurar o diretório app-root/data no classpath do Tomcat. Vamos fazer isso no arquivo catalina.properties, que também fica no diretório {app}/.openshift/config/. Abra o arquivo e procure pela chave common.loader.
yaw@local/~/app/.openshift/config$ gedit catalina.properties

Adicione no fim da propriedade common.loader o conteúdo: ,${catalina.home}/../data/*.jar

A seguir o trecho do properties com essa configuração:
#...
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar
  ,${catalina.home}/lib,${catalina.home}/lib/*.jar
  ,${catalina.home}/../app-root/data/*.jar

Publicar as modificações

Próxima etapa é efetivar as modificações nos arquivos de configuração do Tomcat, vamos fazer um commit e push no repositório:
yaw@local/~/app/$ git commit -m "mysql datasource config"
yaw@local/~/app/$ git push

Após concluir o push (no repositório remoto) você pode notar que o OpenShift reinicializa os serviços (inclusive o Tomcat).

Configuração na aplicação Java

Agora que o DataSource já foi instalado no Tomcat, resta configurá-lo na aplicação Java. Isso pode ser feito no arquivo persistence.xml, assumindo que o projeto utiliza JPA, através da tag non-jta-data-source. Curiosidade, no exemplo a seguir utilizo o Hibernate como provider JPA:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
  xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="appUnit" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <non-jta-data-source>java:/comp/env/jdbc/MysqlDS</non-jta-data-source>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
      <property name="hibernate.show_sql" value="true" />
      <property name="hibernate.hbm2ddl.auto" value="update" />
      <property name="hibernate.connection.charSet" value="UTF-8" />
    </properties>
  </persistence-unit>
</persistence>

Pronto, agora é fazer o commit e o push do código fonte e testar a aplicação no OpenShift. Na YaW desenvolvemos algumas demos no OpenShift.

http://twitter.com/edermag
http://www.yaw.com.br