Revista Java Magazine Edição 56
Revista Java Magazine Edição 56

Agendamentos, persistência e outros recursos avançados

Incremente suas aplicações de Workflow usando recursos avançados do OSWorkflow, como persistência de variáveis e integração com soluções de agendamento.

Esta é a segunda e última parte da série de artigos que visa apresentar a modelagem de fluxos de trabalho em Java. A primeira, apresentada na edição 44, foi focada nos conceitos que envolvem o tema Workflow, apresentando como criar fluxos de trabalho através do OSWorkflow, um engine de workflow open source do grupo OpenSymphony. Neste novo artigo, iremos aprofundar os conhecimentos nesta ferramenta, expandindo o exemplo prático usado anteriormente. A leitura da primeira parte da série é recomendada, mas não é imprescindível: o leitor que não tiver aquela edição deve começar pelo quadro “Relembrando”, que contém todos os conceitos essenciais para acompanhar o presente artigo

Iniciaremos discutindo dois conceitos importantes e bastante freqüentes em fluxos de trabalho: splits e joins. Outra funcionalidade muito adotada relaciona-se com o agendamento de tarefas que deverão ser executadas em um marco no tempo, como a checagem de um estoque, por exemplo. Para esta tarefa, analisaremos a integração do OSWorkflow com o Quartz, uma ferramenta que auxilia na criação e gerenciamento de tarefas. Também utilizaremos duas funcionalidades descritas, embora não utilizadas, no primeiro artigo: ações globais e comuns. Finalizando, discutiremos a integração do OSWorkflow com o PropertySet, permitindo que os dados que trafegam no workflow sejam persistidos. O Quartz e PropertySet são também projetos open source do mesmo grupo, portanto ambos se integram facilmente ao OSWorkflow.

Junções e Separações

Em situações reais constantemente encontramos tarefas que são concluídas através do esforço colaborativo entre os funcionários de uma organização. Algumas delas podem ser executadas em paralelo, visando garantir uma maior produtividade, outras devem ser executadas somente de forma seqüencial. No OSWorkflow a execução de tarefas em paralelo é concretizada através das separações e junções, comumente conhecidas por joins e splits, na terminologia de workflows.

Quando existe a necessidade de se executar dois passos em paralelo, devemos adotar uma separação no nosso fluxo. Visando tornar mais clara esta idéia, ilustraremos com um exemplo que recorre ao sistema de workflow que construímos na primeira parte deste artigo, que pode ser observado na Figura Q1. Suponha que precisamos dividir o passo Análise do Pedido em dois outros: Verificando Estoque e Validando Cliente. Ambos podem ser necessários, pois estas duas tarefas serão executadas por departamentos diferentes em uma organização.

A Figura 1 ilustra como ficará o fluxo após estas alterações. No primeiro passo, Aguardando Análise, estarão todos os pedidos que acabaram de ser criados e que ainda dependem de uma análise prévia, para atestar que não se trata de um pedido gerado incorretamente pelo sistema ou por descuido de algum cliente. Os pedidos aprovados nesta atividade seguem caminhos paralelos. O primeiro tratará do estoque, atestando que há a quantidade solicitada pelo cliente. O segundo certificará que o cliente não possui impedimentos para concretizar o pedido.

Workflow modificado da aplicação de exemplo
Figura 1. Workflow modificado da aplicação de exemplo.

Após aprovado nos dois passos distintos, o pedido retorna para um fluxo seqüencial, através de uma junção. A Listagem 1 apresenta o trecho do código necessário para configurar o OSWorkflow para implementar estes conceitos.

Listagem 1. Definindo as separações e junções.

...

<action id="40" name="Aprovar">

  <results>

    <unconditional-result id="40" old-status="Aprovado" status="Aprovado" join="1"/>

  </results>

</action>

...

<action id="1" name="Registrar Pedido">

  <results>

    <unconditional-result id="2" old-status="Aguardando" status="Confirmado" split="1"/>

  </results>

</action>

...

<splits>

  <split id="1">

    <unconditional-result id="3" old-status="Finished" status="Queued" step="2"/>

    <unconditional-result id="4" old-status="Finished" status="Queued" step="3"/>

  </split>

</splits>

<joins>

  <join id="1">

    <conditions type="AND">

      <condition type="beanshell">

        <arg name="script">

          <![CDATA[       "Aprovado".equals(jn.getStep(2).getStatus())&&"Aprovado".equals(jn.getStep(3).getStatus())

          ]]>

        </arg>

        </condition>

      </conditions>

      <unconditional-result id="5" old-status="Finished" status="Queued" step="4"/>

    </join>

</joins>

Quanto a este trecho, cabe observar alguns detalhes. As definições de separações e junções são criadas pelas tags <join> e <split>. Observe que na definição da separação é necessário que exista a declaração de dois resultados incondicionais, responsáveis por levar o fluxo a outros dois passos. Quanto à junção, neste caso definida através de script BeanShell, temos uma variável nomeada como jn, através da qual é possível obter informações sobre o passo atual. Este script informa que o resultado incondicional só será executado quando as condições descritas forem atendidas.

Finalizando, um resultado avança para uma separação usando o atributo split, enquanto uma junção é realizada através do atributo join. Ressaltamos, também, que OSWorkflow apresenta a limitação de não permitir que o resultado de uma separação ou junção seja outra separação ou junção.

Ações Globais e Comuns

Uma forma de manter o arquivo de definição do fluxo mais limpo e reutilizável consiste em utilizar as ações globais e comuns. Embora a forma de defini-las seja parecida, suas funcionalidades diferenciam-se. A primeira, a ação global, permite que uma ação seja definida e esteja disponível para todos os passos que compõem o workflow. Uma ação comum, por sua vez, define uma ação que poderá ser usada somente pelos passos que a referenciam.

No fluxo proposto, podemos definir uma ação de cada tipo. Uma ação global, que permitirá o cancelamento de um pedido a partir de qualquer ponto do fluxo, e uma ação comum, que permitirá a aprovação de um pedido a partir dos dois novos passos que criamos na seção anterior. A definição da ação global encontra-se na Listagem 2. Conforme pode ser observado, não há nada de diferente na especificação desta ação, somente o ponto no qual ela é declarada, que é dentro da tag <global-actions>.

Listagem 2. Definição da ação global.

<global-actions>

  <action id="30" name="Cancelar">

    <results>

      <unconditional-result id="30" old-status="Cancelado" status="Finalizado" step="7"/>

    </results>

  </action>

</global-actions>

A ação comum, definida na Listagem 3, ilustra a ação Aprovar, que será usada nos passos Verificando Estoque e Validando Cliente. É importante observar nesta listagem a passagem que indica a utilização da ação comum nestes passos. Estas ações são declaradas de forma similar às ações globais, mas possuem como pai a tag <common-actions>.

Listagem 3. Definição da ação comum.

<common-actions>

    <action id="40" name="Aprovar">

      <results>

        <unconditional-result id="40" old-status="Aprovado" status="Finalizado" join="1"/>

      </results>

    </action>

  </common-actions>
  

Tarefas periódicas com o Quartz

O Quartz é um sistema para gerenciamento e execução de tarefas agendadas. Através desta biblioteca é possível determinar trabalhos, ou conjuntos de trabalhos, que serão executados, repetidamente ou não, em um determinado momento no tempo. A integração do Quartz ao OSWokflow permite a um fluxo de trabalho um comportamento temporal, baseado em ações pré-configuradas. Para usar o Quartz neste artigo basta obter a versão mais recente no site oficial e colocá-la no classpath da aplicação.

Para informações mais detalhadas sobre os conceitos e a utilização desta ferramenta, sugerimos o artigo “Tarefas na Hora com Quartz”, edição 41.

A utilização da API do Quartz é bastante simples. Seus principais conceitos são o scheduler, job e trigger. Um scheduler, de forma simplificada, possui como principal responsabilidade gerenciar jobs, que são tarefas definidas pelo usuário. Os dados que indicam quando e quantas vezes um job será executado são especificados através de triggers.

Para utilizar o Quartz em nossa aplicação de exemplo, realizaremos outra modificação no fluxo inicialmente proposto. Consideraremos que o sistema deverá verificar diariamente por pedidos ainda não pagos pelos clientes, realizando o cancelamento daqueles que não tiveram seu valor recebido durante o período de três dias, contados a partir da data da compra. Portanto, conforme ilustra a Figura 1, definiremos um novo passo, nomeado Aguardando Pagamento.

Neste passo incluiremos uma função específica, responsável em verificar os pedidos que deverão ser cancelados. O código responsável por esta verificação consta na Listagem 4, através da classe VerificarPagamentoJob, que implementa a interface Job do Quartz. Esta interface impõe a implementação do método execute() que, neste caso, realizará a verificação dos pedidos pendentes. Seguindo, é necessário informar ao OSWorkflow sobre esta tarefa. A Listagem 5 estende o arquivo XML incluindo uma pre-function ao passo recém criado. Esta pre-function será executada assim que o pedido chegar ao passo Aguardando Pagamento.

Listagem 4. Classe que representa o Job que irá verificar os pedidos pendentes: VerificarPagamentoJob.java.

package br.com.javamagazine.workflow.jobs;

 

import org.quartz.*;

 

public class VerificarPagamentoJob implements Job {

  public void execute(JobExecutionContext jobContext) throws JobExecutionException {

    // Incluir aqui código necessário para obter os pedidos, verificando um a um

    // quais estão pendentes de pagamento por mais de 3 dias.

    long wfId = jobContext.getJobDetail().getJobDataMap().getLong(“entryId”);

    String username = jobContext.getJobDetail().getJobDataMap().getLong(“username”);

    Workflow workflow = new BasicWorkflow(username);

    // Obter informações do workflow para verificar se o pedido deve ser cancelado ou não.

  }

}
Listagem 5. Incluindo a função do agendamento no descritor XML.

<step id="4" name="Aguardando Pagamento">

    <actions>

      <action id="2" name="Confirmar Pagamento" auto="false">

        <results>

               <unconditional-result id="3" old-status="Pago" status="Em Expedicao" step="5"/>        

       </results>

      </action>

    </actions>

    <post-functions>

      <function type="class">

          <arg name="class.name">com.opensymphony.workflow.util.ScheduleJob</arg>

          <arg name="jobClass">br.com.javamagazine.workflow.jobs.VerificarPagamentoJob</arg>

          <arg name="jobName">Verificar Pedidos</arg>

          <arg name="triggerName">Gatilho Pedidos</arg>

          <arg name="triggerId">1</arg>

      <arg name="repeat">REPEAT_INDEFINITALY</arg>

      <arg name="repeatdelay">1</arg>

    </function>

  </post-functions>

</step>

Através da Listagem 4 podemos observar como obter uma referência para a instância do workflow atualmente em execução. O JobDataMap mantém um conjunto de variáveis, incluindo aquelas que incluímos no arquivo de definição do workflow (Listagem 5), através das tags <arg>. O OSWorkflow também inclui outras variáveis, como o identificador do workflow ativo, contido na variável entryId.

Configuramos a execução de nossa tarefa usando a função ScheduleJob, que implementa a interface FunctionProvider, própria do OSWorkflow, passando alguns parâmetros extras. Os mais importantes referem-se ao parâmetro jobClass, que informa qual a classe que será executada; Repeat que informa se esta tarefa deve ser executada mais de uma vez e RepeatDelay, indicando o tempo a ser aguardado antes da tarefa ser executada novamente. Outro método para definir tarefas consta no quadro “Agendando Tarefas com Trigger Functions”.

Dados Persistentes

Determinados processos, em alguns momentos, necessitam manter os dados usados por um período, durante a execução de um fluxo de trabalho. Neste contexto, o OSWorkflow provê formas para se trabalhar tanto com variáveis transientes, como com persistentes. A primeira é obtida através da variável transientVars, fornecida para as funções, condições e scripts. A segunda é obtida através da variável propertySet que, de forma similar, também é fornecida para todas as funções, condições e scripts.

De fato, um propertySet também pode funcionar como um repositório para variáveis transientes. Para que seu comportamento seja alterado, é necessário realizar algumas mudanças em sua configuração, informando o local no qual serão persistidos os valores. Por padrão, estes valores são armazenados em memória.

PropertySet é outro projeto da OpenSymphony, e assemelha-se em muitos aspectos com um java.util.Map. Um propertySet mantém pares chave/valor e diferencia-se apenas nos métodos usados para armazenar estes valores. Os mecanismos de persistência que acompanham este projeto são baseados em JDBC, Hibernate e em memória. Esta ferramenta faz parte da distribuição padrão do OSWorkflow, bastando apenas incluir o arquivo propertyset-1.4.jar no classpath da aplicação.

Para que um propertySet passe a armazenar seu conteúdo em um meio persistente, não é necessário realizar nenhuma alteração em código. Basta realizar algumas configurações extras, em um arquivo XML próprio. Portanto, defina um arquivo propertyset.xml e coloque-o no classpath de sua aplicação. O conteúdo deste arquivo deve ser semelhante ao apresentado na Listagem 6, caso haja a necessidade de se realizar a persistência através de uma conexão JDBC. Outras formas de configuração do propertySet podem ser verificadas na distribuição padrão do OSWorkflow.

Listagem 6. Configurando o PropertySet através do arquivo propertyset.xml

<propertysets>

  <propertyset name="jdbc" class="com.opensymphony.module.propertyset.database.JDBCPropertySet">

    <arg name="datasource" value="jdbc/DefaultDS"/>

    <arg name="table.name" value="OS_PROPERTYENTRY"/>

    <arg name="col.globalKey" value="GLOBAL_KEY"/>

    <arg name="col.itemKey" value="ITEM_KEY"/>

    <arg name="col.itemType" value="ITEM_TYPE"/>

    <arg name="col.string" value="STRING_VALUE"/>

    <arg name="col.date" value="DATE_VALUE"/>

    <arg name="col.data" value="DATA_VALUE"/>

    <arg name="col.float" value="FLOAT_VALUE"/>

    <arg name="col.number" value="NUMBER_VALUE"/>

  </propertyset>

</propertysets>

Agendando Tarefas com Trigger Functions

Como alternativa ao Quartz, outro método eficiente para realizar o agendamento de tarefas consiste em criar funções do tipo Trigger no descritor XML. Trata-se de um tipo especial de função, fornecida pelo OSWorkflow para realizar o agendamento de tarefas. Estas funções não são diretamente associadas a um passo ou ação, e devem ser identificadas de forma única.

Um exemplo de utilização deste tipo de função consta na Listagem 7[Osvaldo P2] . Definimos esta função usando a tag <trigger-function>, que deve ser declarada antes da tag <initial-actions>. Para que esta função seja executada, devemos incluir uma pre-function ou post-function em algum passo ou ação, de forma similar ao exemplo que detecta pagamentos não realizados. Entretanto, devemos preencher o argumento triggerId com o mesmo valor do identificador que usamos na criação da trigger-function. Também é necessário retirar o argumento jobClass, pois o OSWorkflow utilizará uma classe própria, responsável em executar a sua função.

As duas formas para realizar o agendamento de tarefas, apresentadas neste artigo, possuem vantagens e desvantagens que devem ser consideradas. A utilização de uma ou outra fica ao critério do projetista. A forma aqui apresentada permite o acesso às informações do workflow de forma mais simples. Contudo, a segunda forma, que implementa a interface Job do Quartz, permite que as tarefas sejam independentes do OSWorkflow, podendo ser executadas em contextos diferentes.

Listagem 7. Configurando uma função do tipo Trigger.

<trigger-functions>

  <trigger-function id="2">

    <function type="beanshell">

      <arg name="script">

        propertySet.setString("Um exemplo de uso de Trigger Functions.")

      </arg>

    </function>

  </trigger-function>

</trigger-functions>

Relembrando

Na primeira parte desta série de artigos discutimos alguns conceitos de workflows e ilustramos seu uso através do OSWorkflow. Aqui relembraremos de forma sucinta o que foi abordado, apresentando também o exemplo proposto.

De forma simplificada, conceituamos um fluxo de trabalho como um conjunto de passos conectados entre si através de transições. As transições são decorrentes das ações que estão associadas a cada passo. No OSWorkflow estes conceitos são concretizados através de um arquivo descritor em XML. A Listagem 8 apresenta um exemplo bastante simples deste arquivo. Nele incluímos passos através da tag <step> e ações com a <action>. Transições não precisam ser explicitamente definidas, pois são decorrentes da migração de um passo para outro através da execução de uma ação.

Discutimos ainda que as ações são associadas a resultados, que poderão executar tarefas e levar o fluxo a um novo passo. Estes resultados podem ser classificados como condicionais ou incondicionais. O primeiro será executado apenas se as circunstâncias definidas pelo usuário forem satisfeitas, sendo definido pela tag <result>. Um resultado incondicional será executado quando nenhum resultado condicional for satisfeito, sendo representado pela tag <unconditional-result>.

Passos e ações ainda podem ser associados a funções, que permitem ser configuradas para executar antes ou depois da chegada ou saída de uma ação ou passo. Uma função é criada através da tag <function>. Resumindo, um passo indica onde estamos, uma ação indica para onde iremos, dependendo das condições estabelecidas.

Aplicação de Exemplo da primeira parte
Figura Q1. Aplicação de Exemplo da primeira parte.

O exemplo que propusemos neste primeiro artigo encontra-se na Figura Q1. Tratava-se de uma loja de comércio eletrônico, no qual os pedidos realizados pelos clientes trafegavam por este fluxo. Os passos indicavam os pedidos que encontravam-se em análise, no qual os dados do cliente e de estoque eram conferidos; os pedidos em Processamento, no qual era realizada a cobrança; em Expedição, onde encontravam-se os pedidos para serem enviados aos destinatários; e, finalizando, o passo Finalizados, onde ficam todos os pedidos concretizados com sucesso.

Listagem 8. Exemplo simplificado de uso do OSWorkflow.

<workflow>

  <initial-actions>

   <action id="0" name="Iniciar Workflow">

     <results>

      <unconditional-result id="1" old-status="StatusAntigo" status="StatusNovo" step="1"/>

     </results>

   </action>

  </initial-actions>

  <steps>

   <step id="1" name="Passo 1">

     <actions>

      <action id="1" name="UmaAcaoExemplo">

        <results>

         <unconditional-result id="2" old-status="StatusAntigo" status="StatusNovo" step="2"/>

        </results>

      </action>

     </actions>

   </step>

   <step id="2" name="PassoFinal">

     <post-functions>

      <function type="class">

        <arg name="class.name">br.com.javamagazine.UmaClasseFuncao</arg>

      </function>

     </post-functions>

   </step>

  </steps>

</workflow>

Conclusões

Neste artigo ilustramos o uso do OSWorkflow para a criação de fluxos de trabalho. Avançamos em tópicos não discutidos na primeira parte deste artigo, destacando o uso de funcionalidades que permitem a modelagem de fluxos mais complexos. Neste contexto, verificamos como criar junções e separações, visando permitir o trabalho paralelo.

Também definimos tarefas que podem ser executadas automaticamente, e de forma repetida, através da ferramenta Quartz e, finalizando, apresentamos como realizar a persistência dos dados que trafegam no fluxo. Com os principais conceitos e recursos do OSWorkflow apresentados, você será capaz de desenvolver aplicações baseadas em fluxos de trabalho com maior rapidez, flexibilidade e robustez.

Os novos trechos de código apresentados neste artigo complementam aqueles discutidos na primeira parte desta série. Visando evitar repetições, os trechos anteriores não foram listados aqui, contudo, o projeto está disponível no download desta edição. A Figura 2 ilustra como ficará a nova estrutura do projeto.

Estrutura do projeto contendo as novas classes e arquivos de configuração
Figura 2. Estrutura do projeto contendo as novas classes e arquivos de configuração.

Este exemplo foi desenvolvido no Eclipse, para utilizá-lo, basta importar o projeto nesta ferramenta, utilizando a opção do menu File | Import e selecionando, logo em seguida, o item General > Existing Projects Into Workspace. Para ver o exemplo em funcionamento, abra a classe Gerenciador e selecione a opção do menu Run | Run As > Java Application. Após a execução, você verá uma saída no console igual à apresentada na Figura 3.

Resultado após a execução do exemplo
Figura 3. Resultado após a execução do exemplo.

É possível observar, nesta figura, a migração entre os passos ocorrendo, assim como o agendamento da tarefa pelo Quartz. Observe que a aplicação ficará sendo executada infinitamente, até que você a faça parar de forma manual. Isto ocorre, pois configuramos, no descritor, para que nossa tarefa seja executada indefinidamente.