Java – Bouncy Castle – “Extended Key Usages”de um certificado X.509

Estou compartilhando o que aprendi. Então você provavelmente já viu isso no Stack Overflow ou encontrou numa pesquisa no Google.

De acordo com o RFC 5280 o Extended Key Usage “indica uma ou mais finalidades para as quais a chave pública de um certificado pode ser usada, em conjunto ou no lugar das finalidades básicas indicadas na extensão ‘key usage'”. Se um certificado possui essa extensão, ele deve ser usado apenas para um dos propósitos indicados.

Um certificado que possui as extensões “key usages” e “extended key usages” deve ser usado apenas para um propósito consistente com o definido para ambas as extensões.

O “EU Digital COVID Certificate” é um bom exemplo do uso de “extended key usages”. Eles utilizam essa extensão para definir qual tipo de documento pode ser assinado por um certificado X.509. Uma aplicação que verifica a validade de um documento “EU DCC” vai entender como inválido um documento assinado por um certificado que não possua o “extended key usages” apropriado.

A seção “4.2.1.12. Extended Key Usage” da RFC 5280 contém a documentação oficial da extensão.

O código neste post, foi elaborado utilizado a biblioteca Bouncy Castle na versão 1.72. Se você estiver usando uma versão anterior, o código pode não compilar.

Eu uso Gradle então, as dependências estão no formato dele:

implementation "org.bouncycastle:bcprov-jdk18on:1.72"
implementation "org.bouncycastle:bcpkix-jdk18on:1.72"
implementation "org.slf4j:slf4j-api:2.0.7"

The code I use for retrieve key usages is:

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.X509CertificateHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger LOG = LoggerFactory.getLogger("example");
	
public static List<String> extendKeyUsages(final X509Certificate x509) 
{
	if (x509 == null) 
	{
		return Collections.emptyList();
	} 
	else 
	{
		final List<String> rtn = new ArrayList<>();
		try
		{
			final X509CertificateHolder certHolder = new X509CertificateHolder(x509.getEncoded());
			final ExtendedKeyUsage extKeyUsage = ExtendedKeyUsage.fromExtensions(certHolder.getExtensions());
			
			if (extKeyUsage != null && extKeyUsage.getUsages() != null)
			{
				final KeyPurposeId[] kpi = extKeyUsage.getUsages();
				Arrays.stream(kpi).forEach(item -> rtn.add(item.getId()));
			}
		}
		catch (Exception e)
		{
			LOG.warn("{} :: {}", e.getClass().getName(), e.getMessage());
		}
		return rtn;
	}
}

O método retorna um “List<String>” que contém o “id” de cada “extended key usages” definido para um certificado X.509. Em caso de qualquer exceção, retorna uma lista vazia.

Você pode usar o código “como está” ou alterar para atender seus requisitos.

Java - Bouncy Castle – “Key usages” de um Certificado X.509

Estou compartilhando o que aprendi. Então você provavelmente já viu isso no Stack Overflow ou encontrou numa pesquisa no Google.

De acordo com o RFC 5280, “a extensão ‘key usage’ define a finalidade (por exemplo codificação, assinatura) da chave contida no certificado”. Isso quer dizer uma restrição de uso pode, ou deveria, ser aplicada e verificada ao usar a chave.

O uso da chave (“key usage”) também pode, ou deveria, ser aplicado e verificado para garantir, por exemplo, se ao ser usada para assinar um conteúdo (assinatura digital) possui as finalidades “digitalSignature” and “nonRepudiation” foram atribuídas a ela.

A seção “4.2.1.3. Key Usage” da RFC 5280 descreve a finalidade de cada “key usage”. Essa é a documentação oficial, então use-a para definir suas próprias políticas para aceitar ou rejeitar a chave de um certificado X.509.

O código neste post, foi elaborado utilizado a biblioteca Bouncy Castle na versão 1.72. Se você estiver usando uma versão anterior, o código pode não compilar.

Eu uso Gradle então, as dependências estão no formato dele:

implementation "org.bouncycastle:bcprov-jdk18on:1.72"
implementation "org.bouncycastle:bcpkix-jdk18on:1.72"
implementation "org.slf4j:slf4j-api:2.0.7"

Eis o código que uso para recuperar o “key usage” de uma chave pública:

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.X509CertificateHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger LOG = LoggerFactory.getLogger("example");
	
public static List<String> keyUsages(final X509Certificate x509) 
{
    final List<String> rtn = new ArrayList<>();
    
    if (x509 == null) 
    {
        return rtn;
    } 
    else 
    {
        final Map<Integer, String> keyUsagesMap = new LinkedHashMap<>();
        keyUsagesMap.put(KeyUsage.digitalSignature, "digitalSignature");
        keyUsagesMap.put(KeyUsage.nonRepudiation,   "nonRepudiation");
        keyUsagesMap.put(KeyUsage.keyEncipherment,  "keyEncipherment");
        keyUsagesMap.put(KeyUsage.dataEncipherment, "dataEncipherment");
        keyUsagesMap.put(KeyUsage.keyAgreement,     "keyAgreement");
        keyUsagesMap.put(KeyUsage.keyCertSign,      "keyCertSign");
        keyUsagesMap.put(KeyUsage.cRLSign,          "cRLSign");
        keyUsagesMap.put(KeyUsage.encipherOnly,     "encipherOnly");
        keyUsagesMap.put(KeyUsage.decipherOnly,     "decipherOnly");
        
        try
        {
            final X509CertificateHolder certHolder = new X509CertificateHolder(x509.getEncoded());
            final KeyUsage kUsage = KeyUsage.fromExtensions(certHolder.getExtensions());
            
            if (kUsage != null)
            {
                byte[] data = kUsage.getBytes();

                if (data.length == 1)
                {
                    rtn.add("0x" + Integer.toHexString(data[0] & 0xff));
                }
                else
                {
                    rtn.add("0x" + Integer.toHexString((data[1] & 0xff) << 8 | (data[0] & 0xff)));
                }
                
                keyUsagesMap.entrySet().forEach(item ->
                {
                    if (kUsage.hasUsages(item.getKey())) 
                    {
                        rtn.add(item.getValue());
                    }
                });
            }
        }
        catch (Exception e)
        {
            LOG.warn("{} :: {}", e.getClass().getName(), e.getMessage());
        }
        return rtn;
    }
}

O método retorna um “List<String>” que contém a “descrição” de cada “key usage” suportado pela chave pública de um certificado no formato X.509. No caso de qualquer exceção, o método retorna uma lista vazia.

Você pode usar o código “como está” ou alterar para atender seus requisitos.

Java - Identificar um Certificado X.509 Autoassinado

Estou compartilhando o que aprendi. Então você provavelmente já viu isso no Stack Overflow ou encontrou numa pesquisa no Google.

De acordo com a Wikipedia: “… certificados autoassinados (Self-signed certificate) são certificados de chave pública que não são emitidos por uma autoridade de certificação (CA). Esses certificados autoassinados são fáceis de fazer e não custam dinheiro. No entanto, eles não fornecem valor de confiança“.

Eles são úteis para uso interno e desenvolvimento mas devem ser evitados para quaisquer outros usos.

Esta é uma forma (dentre várias) para identificar um certificado do tipo X.509 with Java:

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public static boolean ehAutoAssinado(final X509Certificate cert)
    throws CertificateException, NoSuchAlgorithmException, NoSuchProviderException 
{
  try 
  {
    // Tentando verificar se foi assinado por sua própria chave pública
    final PublicKey key = cert.getPublicKey();
    cert.verify(key);
    return true;
  } 
  catch (SignatureException sigEx) 
  {
    // Assinatura inválida --> Não é autoassinado
    return false;
  } 
  catch (InvalidKeyException keyEx) 
  {
    // Assinatura inválida --> Não é autoassinado
    return false;
  }
}

O código acima usa API JDK padrão.

Oracle BPM Quick Start Domain

Ao instalar o quick start do BPM Suite e iniciar o domínio embutido via JDeveloper 12c observei várias falhas na incialização. Quando fui acessar o BPM Workspace nada funcionava.

Analisando log com mais atenção observei:

Falhas que podem ser ignoradas

As exceções semelhantes aos trechos mostrados abaixo podem ser ignoradas. Elas não vão influenciar no funcionamento da ferramenta:

SEVERE: failure initializing ADR
oracle.dfw.common.DiagnosticsException: failure loading adrci binary from the system path: null
Cause: DFW-40112: There was an error executing adrci commands; the following errors have been found "null"
Action: Ensure that command line tool "adrci" can be executed from the command line.
<Emergency> <oracle.jps.idmgmt> <JPS-01520> <Cannot initialize identity store, cause: oracle.security.idm.ConfigurationException: javax.naming.CommunicationException: 10.1.4.43:7001 [Root exception is java.net.ConnectException: Connection refused (Connection refused)
<Error> <org.quartz.core.ErrorLogger> <BEA-000000> <An error occured while scanning for the next trigger to fire.
org.quartz.JobPersistenceException: Couldn't acquire next trigger: null [See nested exception: java.lang.reflect.UndeclaredThrowableException
...
Caused By: java.sql.SQLSyntaxErrorException: Syntax error: Encountered "SKIP" at line 1, column 210.

Falha que não pode ser ignorada

<Jun 12, 2019 4:33:42,900 PM BRT> <Error> <Deployer> <BEA-149205> <Failed to initialize the application "EDNDataSource" due to error weblogic.application.ModuleException: java.net.ConnectException: Connection refused (Connection refused)
weblogic.application.ModuleException: java.net.ConnectException: Connection refused (Connection refused)

Esta falha sim é problema e vai acontecer para todos os datasources que são necessários para a infraestrutura SOA funcionar corretamente e isso acontece porque os scripts de inicialização do Weblogic não conseguiram iniciar o Apache Derby.

O método mais simples que consegui encontrar para resolver o problema foi fazer o download o Apache Derby a partir do site oficial (a versão que vem empacotada no quick start é a 10.11.1.1) e substituir os binários dentro da instalação do Weblogic.

Para substituir os binários do Apache Derby que vieram com o quick start acesse o diretório $MW_HOME/common.

Renomeie o diretório “derby” para “derby_old” (não apague apenas troque o nome).

Descompacte o arquivo com os binários do Apache Derby no diretório “$MW_HOME/common” e troque o nome para “derby” ($MW_HOME/common/derby).

Agora vem o passo mais importante.

A partir do diretório “$MW_HOME/common/derby_old/bin” copie os arquivos abaixo para o diretório “$MW_HOME/common/derby/bin”:

  • startNetworkServer.cmd
  • startNetworkServer.sh
  • stopNetworkServer.cmd
  • stopNetworkServer.sh

Para o diretório “derby/bin”.

Esses quatro arquivos são utilizados pelos scripts de inicialização do Weblogic para iniciar e parar o Apache Derby e criar o repositório para o funcionamento do BPM Suite.

Inicie novamente o servidor.

Agora você vai observar a seguinte falha nos logs:

<Jun 12, 2019 5:25:18,598 PM BRT> <Error> <oracle.soa.services.workflow.persistency> <BEA-000000> <<.> exception.code:30257
exception.type: ERROR
exception.severity: 2
exception.name: Error while Querying workflow task metadata.
exception.description: Error while Querying workflow task metadata.
exception.fix: Check the underlying exception and the database connection information.  If the error persists, contact Oracle Support Services.

ORABPEL-30257

exception.code:30257
exception.type: ERROR
exception.severity: 2
exception.name: Error while Querying workflow task metadata.
exception.description: Error while Querying workflow task metadata.
exception.fix: Check the underlying exception and the database connection information.  If the error persists, contact Oracle Support Services.

Caused By: ERROR 42X04: Column 'WFTM.PACKAGENAME' is either not in any table in the FROM list or appears within a join specification and is outside the scope of the join specification or appears in a HAVING clause and is not in the GROUP BY list. If this is a CREATE or ALTER TABLE  statement then 'WFTM.PACKAGENAME' is not a column in the target table.

Sem parar o servidor, siga as instruções deste link https://blog.darwin-it.nl/2017/12/bpm-12213-exception-when-deploying-bpm.html para resolver o problema.

Com esses passos o servidor deve funcionar como esperado.

Qualquer dúvida deixe um comentário.

Oracle Service BUS 12c :: “Failed to parse XML text”

Recentemente comecei a utilizar o Oracle Service BUS 12c, mais precisamente a versão 12.2.1.0, e um dos objetivos era o de criar um webservice REST a partir de um webservice SOAP através de uma configuração de OSB.

Virtualizar o serviço SOAP e criar o respectivo serviço REST utilizando o OSB 12c foi bem mais fácil do que eu esperava, a maior parte do trabalho foi “arrastar-e-soltar“.

Realizei os testes testes do serviço SOAP e da interface REST a partir do console OSB e obtive os resultados esperados.

Para ter certeza que não estava me apegando ao resultado aprestado pelo SB Console

Entretanto, quando realizei os mesmos testes na interface REST utilizando o Postman (e o SoapUI) obtinha como resposta a mensagem de falha:

{
  "errorMessage": "Failed to parse XML text",
  "errorCode": "OSB-382000"
}

Analisei o log do servidor para descobrir a causa do problema mas ele apenas apresentava o seguinte:

<com.bea.alsb.common.catalog.cataloglogger> <cataloglogger> <log>
<exception in="" inboundwsdlresthandler.handleresponse,="" com.bea.wli.sb.sources.transformexception:=""  failed="" to="" parse="" xml="" text="">
...
Caused by: java.io.CharConversionException: Malformed UTF-8 character: 0xc1 0x47

Como você pode ver, o log do servidor não ajudou a descobrir porque o problema aconteciae pesquisar no Google e no “My Oracle Support” também não ajudaram muito.

Depois de horas de pesquisas infrutiferas decidi fazer um teste utilizando um processo BPEL e, como suspeitava, tive certeza que o problema era causado pelo enconding do serviço SOAP.

Neste caso a solução foi alterar as informações do business service, do serviço SOAP que estava virtualizado, definindo o encoding para o request e response do componente na configuração do OSB.

Para ajustar isso clique com o botão direito sobre o componente e selecione a opção “Edit”, como mostrado na imagem abaixo:

arcgis1

Na tela de edição do arquivo “.bix”, no menu a esquerda, selecione o item “Transport Details” e informe o valor “utf-8” nos campos “Request Encoding” e “Response Encoding” como mostrado na figura abaixo:

arcgis2

Com esse ajuste na configuração do “business service” a interface REST  vai funcionar como esperado.

Qualquer dúvida deixa um comentário.

 

Atualizando o Client SVN do JDeveloper 11g R1

Atualmente estou trabalhando com ferramentas do Fusion Middleware da Oracle (SOA Suite, BPM Suite, WebCenter, etc) e o Oracle JDeveloper 11g R1 (mais precisamente a versão 11.1.1.5) é minha principal ferramenta de desenvolvimento. Essa ferramenta já possui um cliente de SVN embutido entretanto, a versão disponível atualmente ainda carece de funcionalidades comuns no TortoiseSVN e por essa razão muitos de nós prefere utilizar o TortoiseSVN ao invés do cliente interno do JDeveloper.

Uma das razões que me faz preferir o TortoiseSVN ao cliente do JDeveloper são suas atualizações frequentes.

Portanto, se você assim como eu atualizou seu TortoiseSVN para a versão 1.7.1 e passou a ter problemas com o JDeveloper para abrir seus projetos faça o seguinte:

  1. Realize o download da versão “Standalone” do SVNKit (http://svnkit.com/) e descompacte o arquivo (aparecerá o diretório “svnkit-1.3.6-v1“).
  2. Renomeie os arquivos como mostrado abaixo (os arquivos estão no diretório ” lib “):
    de para
    svnkit-javahl16-1.3.6-v1.jar svnjavahl.jar
    svnkit-1.3.6-v1.jar svnkit.jar
    trilead-ssh2-1.0.0-build214.jar trilead.jar
  3. Agora copie os arquivos renomeados para  [DIR_INSTALL_JDEVELOPER]\jdev\extensions\oracle.jdeveloper.subversion“. Substituia os arquivos originais.
  4. Copie os arquivos de licença (diretório ” licenses “)

Agora inicie novamente o JDeveloper que e ele volta a funcionar normalmente.

Antes da atualização quando você acessava o menuo ” Tools\Preferences\Versioning ” o JDeveloper mostrava “SVNKit/1.3.0 with JNA Disabled” após a atualização vai passar a mostrar “SVNKit/1.3.6 with JNA Disabled“.

A título de curiosidade, comigo, a falha que estava acontecendo no JDeveloper por causa do SVNKit desatualizado acontecia sempre que eu tentava acessar o menu “Application”.

Elaborei esse post utilizando como insumo um outro elaborado por “Aino Andriessen” para o JDeveloper 10g com o título “Upgrade JDeveloper 10g Subversion client“.

Qualquer dúvida, por favor, poste um comentário.

Internacionalização (i18N) com JBOSS SEAM e RichFaces

Por mais que esse seja um assunto já bastante explorado em outros blogs e na documentação de vários componentes, o que vou mostrar aqui auxilia na utilização do Hibernate Validations para validação da entrada de dados em aplicações JSF usando RichFaces.

Se você também usa o RichFaces com Hibernate Validations deve estar recebendo as mensagens em inglês ao invés de português. Bom depois de muita pesquisa vendo “MessagesInterpolator”, depurando, código e etc eu tentei definir a internacionalização tanto pelo JBoss Seam quanto pelo JSF (faces-config.xml) e para minha surpreza as mensagens passaram a ser exibidas em português.

A solução começa por configurar a inicialização do JBOSS definindo “Português-Brasil” como o idioma e local padrão. Isso é feito adicionando após a linha 77 no arquivo “JBOSS_HOME\bin\run.bat” a linha mostrada abaixo (linha destacada):

set JAVA_OPTS=%JAVA_OPTS% -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000
set JAVA_OPTS=%JAVA_OPTS% -Duser.language=pt -Duser.country=BR

Apenas isso já resolve o “problema” de exibir mensagens em inglês onde deveriam estar aparecendo em português entretanto, algumas vezes temos que dar suporte a outros idiomas como inglês e espanhol e apenas com esse ajuste de configuração a aplicação vai continuar mostrando as mensagens em português quando deveria estar mostrando em inglês ou espanhol.

Para evitar esse problema e recuperar o idioma que deve ser usado de acordo com o navegador utilizado pelo cliente, temos agora que ajustar a configuração do arquivo “components.xml” do conforme mostrado abaixo:

<components xmlns="http://jboss.com/products/seam/components"
	xmlns:core="http://jboss.com/products/seam/core" xmlns:drools="http://jboss.com/products/seam/drools"
	xmlns:mail="http://jboss.com/products/seam/mail" xmlns:persistence="http://jboss.com/products/seam/persistence"
	xmlns:security="http://jboss.com/products/seam/security" xmlns:web="http://jboss.com/products/seam/web"
	xmlns:i18n="http://jboss.com/products/seam/international"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.2.xsd
	                  http://jboss.com/products/seam/persistence
	                  http://jboss.com/products/seam/persistence-2.2.xsd
	                  http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.2.xsd
	                  http://jboss.com/products/seam/bpm http://jboss.com/products/seam/bpm-2.2.xsd
	                  http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd
	                  http://jboss.com/products/seam/mail http://jboss.com/products/seam/mail-2.2.xsd
	                  http://jboss.com/products/seam/web http://jboss.com/products/seam/web-2.2.xsd
	                  http://jboss.com/products/seam/i18n http://jboss.com/products/seam/international-2.0.xsd
	                  http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd">

	<!-- As linhas abaixo devem ser adicionadas no seu components.xml -->

	<web:character-encoding-filter encoding="UTF-8" override-client="true" url-pattern="*.seam" />

	<i18n:locale-config default-locale="br" supported-locales="br en es"/>

</components>

Agora que o SEAM já está preparado para tratar a localização do usuário temos que adicionar o suporte a essas linguagens no arquivo “faces-config.xml” da aplicação como abaixo:

<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
	
	<!-- A tag abaixo deve ser alterada. -->
	
	<application>
		<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
		<locale-config>
			<default-locale>br</default-locale>
			<supported-locale>br</supported-locale>
			<supported-locale>en</supported-locale>
			<supported-locale>es</supported-locale>
		</locale-config>
	</application>
</faces-config>

Concluídas essas configurações a aplicação se comportará como esperado em relação a internacionalização e, principalmente, as mensagens do Hibernate Validations serão apresentadas no idioma esperado.

Por último, mas não menos importante, essas são as versões do que usei para elaborar esta publicação:

  • RichFaces 3.3.3
  • JBOSS SEAM 2.2.1
  • JBOSS Application Server 4.2.3 GA e 5.1.0 GA

Qualquer dúvida, por favor, deixe um comentário.

Oracle ADF Code Corner – Exemplos Práticos

Se você assim como eu está tendo dificuldades para implementar algum comportamento específico usando Oracle ADF, acesse o “Oracle ADF Code Corner” no link “http://www.oracle.com/technetwork/developer-tools/adf/learnmore/index-101235.html“.

Esse site me chamou a atenção principalmento por causa do item “023. How-to build a Generic Selection Listener for ADF bound Tables” que me ajudou na implementação de uma prova de conceito.

Qualquer dúvida, deixe um comentário.

Inicializando o JBoss como serviço do Windows

Ontem precisei configurar um JBoss para ser inicializado com um serviço do Windows e para isso usei o “JBoss Web Native Connectors“.

Os procedimentos que vou mostrar estão orientados ao JBoss 4.2.3, porque essa versão não vem com o utilitário para instalar o serviço do windows no pacode de download padrão.

Então vamos aos procedimentos:

  1. Faça o download do “JBoss Web Native Connectors” versão 2.0.8 correspondente ao seu sistema operacional (Windows 32 ou 64 bits).
  2. Descompacte o arquivo “jboss-native-2.0.8-windows-x64-ssl.zip” (no caso de um Windows 64 bits) e copie apenas os arquivos do diretório “bin” (“README-service.txt“, “jbosssvc.exe“, “jbossweb.x64.exe“, “jbosswebw.x64.exe” e “service.bat“)  para “JBOSS_HOME\bin“.
  3. Edite o arquivo “service.bat” alterando as linhas 75 e 104, de “call run.bat < .r.lock >> run.log 2>&1” para “call run.bat -b 0.0.0.0 -c default < .r.lock >> run.log 2>&1“. Explicando a linha de comando:
    • -b 0.0.0.0 => é para indicar que o servidor pode ser acessado de qualquer máquina na rede.
    • -c default => indica que a configuração de servidor a ser iniciada é a “default” (as configurações de servidor do JBoss disponíveis ficam em “JBOSS_HOME\server“).
  4. No prompt de comando acesse o diretório “JBOSS_HOME\bin” e execute o comando “service.bat install“.

Com os passos mostrados anteriormente você vai ter um serviço instalado no Windows para o JBoss com o nome “JBAS50SVC“. Para alterar o nome do serviço que será instalado você deve, antes de executar os passos descritos, alterar as linhas 20, 21 e 22 do arquivo “service.bat” com o nome e descrição mais adequados para o serviço.

Para inicializar as versões do JBoss 5.x.x como serviço do windows você precisa apenas executar os passos 3 e 4 pois essas versões já vem com o utilitário de instalação do serviço.

A fonte que utilizei para esse tutorial foi um post no blog da Adobe com o título “Running LiveCycle on JBoss as a 64-bit Windows Service” portanto, se você quiser consultar minha fonte acesse este link.

No caso do Windows 2008, Windows 7 e Windows Vista lembre-se de executar o prompt de comando como administrador (botão direito “Executar como administrador”) pois se você esquecer desse detalhe o serviço não vai ser instalado corretamente, portanto, muita atenção com esse detalhe.

Qualquer dúvida deixe um comentário.

— Atualização em 21/08/2013

Pessoal, já faz algum tempo que eu elaborei esse post.

Pelo que vi ele tem ajudado muita gente e fico extremamente feliz por isso mas, na época que eu o escrevi a versão estável do JBOSS já era a versão 6 e a 4.2.3 já havia sido “descontinuada” (sem novas releases) a mais de 2 anos.

Levando em conta que já são quase 3 anos deste post e 5 da versão alvo dele e que a versão atual é a 7.1 decidi por “fechar” o post para novos comentários.

Obrigado por terem lido.

Herberson

Uma solução para saber quem está editando com JBoss Seam

Semana passada eu tive que implementar o seguinte requisito:

Um registro não pode ser visualizado por usuários distintos ao mesmo tempo.

Explicando melhor, isso quer dizer que dois usuários não podem visualizar o detalhe de um determinado registro ao mesmo tempo.

A solução mais simples é criar um componente com escopo “APPLICATION” e colocar nele o código que será responsável por esse gerenciamento. E, é exatamente o código que elaborei para fazer esse trabalho que vou mostrar aqui.

Nessa solução eu usei um EJB Statefull, que pode ser adaptada para um JavaBean, eis o código:

Da interface:

package br.com.herberson.core;

import javax.ejb.Local;

import br.com.herberson.entity.User;
import br.com.herberson.entity.concept.BaseConcept;

@Local
public interface ConceptOnUpdate {
	void initialize();
	Boolean onUpdate(BaseConcept concept);
	String identifyUpdater(BaseConcept concept);
	Boolean onUpdateByUser(BaseConcept concept, User userUpdating);
	void releaseConceptsFromUser(User userUpdating);
	void releaseConcept(BaseConcept concept, User userUpdating);
	void putOnUpdate(BaseConcept concept, User userUpdating);
	String toString();
	void destroy();
}

Da Implementação:

package br.com.herberson.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import javax.ejb.Remove;
import javax.ejb.Stateful;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.Synchronized;
import org.jboss.seam.log.Log;

import br.com.herberson.entity.User;
import br.com.herberson.entity.concept.BaseConcept;
import br.com.herberson.entity.concept.CustomerConcept;
import br.com.herberson.entity.concept.MaterialConcept;
import br.com.herberson.entity.concept.ProductConcept;
import br.com.herberson.entity.concept.VendorConcept;
import br.com.herberson.util.Bean;

@Name("conceptOnUpdate")
@Synchronized(timeout=3000)
@Scope(ScopeType.APPLICATION)
@Startup(depends="entityManager")
@AutoCreate
@Stateful
public class ConceptOnUpdateImpl implements ConceptOnUpdate {
	private static final long serialVersionUID = 1L;

	/**
	 * Map com a lista de materiais em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> materialOnUpdate;

	/**
	 * Map com a lista de produtos em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> productOnUpdate;

	/**
	 * Map com a lista de clientes em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> customerOnUpdate;

	/**
	 * Map com a lista de fornecedores em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> vendorOnUpdate;

	/**
	 * Índice no array de values correspondente à posição do nome do usuário.
	 */
	private int USER_NAME = 0;

	/**
	 * Índice no array de values correspondente à posição ao ID da sessão do usuário.
	 */
	private int SESSION_ID = 1;

	@Logger // NOPMD by herberson on 07/07/10 11:25
	private Log log; // NOPMD by herberson on 07/07/10 11:26

	/**
	 * Initializa as tabelas de objetos em atualização.
	 */
	@Create
	public void initialize() {
		if (materialOnUpdate == null) {
			log.info("Method initialize():void - materialOnUpdate");
			materialOnUpdate = new HashMap<Long, String[]>();
		} //end if
		if (productOnUpdate == null) {
			log.info("Method initialize():void - productOnUpdate");
			productOnUpdate = new HashMap<Long, String[]>();
		} //end if
		if (customerOnUpdate == null) {
			log.info("Method initialize():void - customerOnUpdate");
			customerOnUpdate = new HashMap<Long, String[]>();
		} //end if
		if (vendorOnUpdate == null) {
			log.info("Method initialize():void - vendorOnUpdate");
			vendorOnUpdate = new HashMap<Long, String[]>();
		} //end if
	} //end method

	/**
	 * Verifica se o objeto está sendo atualizado.
	 *
	 * @param concept
	 * @return
	 */
	public Boolean onUpdate(BaseConcept concept) {
		log.info("Method onUpdate(concept):Boolean");
		initialize();
		return Bean.isNotNull(identifyUpdater(concept));
	} //end method

	/**
	 * Identifica o usuário que está atualizando o registro.
	 *
	 * @param concept
	 * @return
	 */
	public String identifyUpdater(BaseConcept concept) {
		log.info("Method identifyUpdater(concept):String");
		initialize();
		return getValue(concept)[USER_NAME];
	} //end method

	/**
	 * Verifica se o usuário informado no argumento <code>userUpdating</code> é o
	 * mesmo que está atualizando o registro.
	 *
	 * @param concept
	 * @param userUpdating
	 * @return
	 */
	public Boolean onUpdateByUser(BaseConcept concept, User userUpdating) {
		log.info("Method onUpdateByUser(concept, userUpdating):Boolean");
		initialize();
		boolean onUpdate;
		String[] value;

		value = getValue(concept);

		if (value[USER_NAME].equals(userUpdating.getUserName())
				&& value[SESSION_ID].equals(userUpdating.getHttpSessionId())) {
			onUpdate = true;
		} else {
			onUpdate = false;
		} //end if

		return onUpdate;
	} //end method

	/**
	 * Libera todos os registros travados para o usuário.
	 *
	 * @param userUpdating
	 */
	public void releaseConceptsFromUser(User userUpdating) {
		log.info("Method releaseConceptsFromUser(userUpdating):void - Start...");
		initialize();
		ArrayList<Long> customerToRemove = new ArrayList<Long>();
		ArrayList<Long> materialToRemove = new ArrayList<Long>();
		ArrayList<Long> productToRemove = new ArrayList<Long>();
		ArrayList<Long> vendorToRemove = new ArrayList<Long>();

		for (Map.Entry<Long, String[]> entry : customerOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				customerToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Map.Entry<Long, String[]> entry : materialOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				materialToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Map.Entry<Long, String[]> entry : productOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				productToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Map.Entry<Long, String[]> entry : vendorOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				vendorToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Long keyToRemove : customerToRemove) {
			customerOnUpdate.remove(keyToRemove);
		} //end for

		for (Long keyToRemove : materialToRemove) {
			materialOnUpdate.remove(keyToRemove);
		} //end for

		for (Long keyToRemove : productToRemove) {
			productOnUpdate.remove(keyToRemove);
		} //end for

		for (Long keyToRemove : vendorToRemove) {
			vendorOnUpdate.remove(keyToRemove);
		} //end for
		log.info("Method releaseConceptsFromUser(userUpdating):void - End...");
	} //end method

	/**
	 * Libera um determinado registro que estava travado para o usuário.
	 *
	 * @param concept
	 */
	public void releaseConcept(BaseConcept concept, User userUpdating) {
		log.info("Method releaseConcept(concept, userUpdating):void - Start...");
		initialize();
		if (onUpdateByUser(concept, userUpdating)) {
			if (concept instanceof MaterialConcept) {
				materialOnUpdate.remove(getKey(concept));
			} else if (concept instanceof ProductConcept) {
				productOnUpdate.remove(getKey(concept));
			} else if (concept instanceof VendorConcept) {
				vendorOnUpdate.remove(getKey(concept));
			} else if (concept instanceof CustomerConcept) {
				customerOnUpdate.remove(getKey(concept));
			} //end if
		} //end if
		log.info("Method releaseConcept(concept, userUpdating):void - End...");
	} //end method

	/**
	 * Trava o registro para atualização.
	 *
	 * @param concept
	 * @param userUpdating
	 */
	public void putOnUpdate(BaseConcept concept, User userUpdating) {
		log.info("Method putOnUpdate(concept, userUpdating):void - Start...");
		initialize();
		String[] value;
		Long key;

		if (Bean.isNotNull(userUpdating.getHttpSessionId())) {
			value = new String[]{userUpdating.getUserName(), userUpdating.getHttpSessionId()};
		} else {
			value = new String[]{userUpdating.getUserName(), ""};
		} //end if

		key = getKey(concept);

		if (concept instanceof MaterialConcept) {
			materialOnUpdate.put(key, value);
		} else if (concept instanceof ProductConcept) {
			productOnUpdate.put(key, value);
		} else if (concept instanceof VendorConcept) {
			vendorOnUpdate.put(key, value);
		} else if (concept instanceof CustomerConcept) {
			customerOnUpdate.put(key, value);
		} //end if
		log.info("Method putOnUpdate(concept, userUpdating):void - End...");
	} //end method

	/**
	 * Monta a chave para o Map de objetos travados.
	 *
	 * @param concept
	 * @return
	 */
	private Long getKey(BaseConcept concept) {
		initialize();
		Long key;

		if (concept instanceof MaterialConcept) {
			key = ((MaterialConcept)concept).getMaterialId();
		} else if (concept instanceof ProductConcept) {
			key = ((ProductConcept)concept).getProductId();
		} else if (concept instanceof CustomerConcept) {
			key = ((CustomerConcept)concept).getCustomerId();
		} else if (concept instanceof VendorConcept) {
			key = ((VendorConcept)concept).getVendorId();
		} else {
			key = Long.MIN_VALUE;
		} //end if

		return key;
	} //end method

	/**
	 * Recupera o valor para o Map de objetos travados.
	 * @param concept
	 * @return
	 */
	private String[] getValue(BaseConcept concept) {
		initialize();
		String[] value;

		if (concept instanceof MaterialConcept) {
			value = materialOnUpdate.get(getKey(concept));
		} else if (concept instanceof ProductConcept) {
			value = productOnUpdate.get(getKey(concept));
		} else if (concept instanceof CustomerConcept) {
			value = customerOnUpdate.get(getKey(concept));
		} else if (concept instanceof VendorConcept) {
			value = vendorOnUpdate.get(getKey(concept));
		} else {
			value = materialOnUpdate.get(getKey(concept));
		} //end if

		if (value == null) {
			value = new String[]{"", ""};
		} //end if

		return value;
	} //end method

	@Override
	public String toString() {
		initialize();
		StringBuffer string = new StringBuffer();

		string.append("MATERIAL: ");
		string.append(materialOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		string.append("PRODUCT: ");
		string.append(productOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		string.append("VENDOR: ");
		string.append(vendorOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		string.append("CUSTOMER: ");
		string.append(customerOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		return string.toString();
	} //end method

	@Destroy
	@Remove
    public void destroy() {
		log.info("Method destroy():void");
	} //end method

} //end class

Agora a explicação sobre o componente

Como as informações do componente devem estar disponíveis para todos os usuários autenticados na aplicação o escopo escolhido foi “APPLICATION”, o uso da anotação “@AutoCreate” foi para forçar a criação do componente na inicialização (deploy) da aplicação, o uso da anotação “@Startup” foi para evitarmos que o componente esteja indisponível sem uma conexão de banco de dados ativa e por último a anotação “@Synchronized” foi para evitar a exceção “java.util.ConcurrentModificationException” ao manipularmos os maps com os registros “travados”.

Para evitar identificadores iguais eu crieu um map (java.util.Map) para cada conceito controlado e dessa forma, apesar de mais código escrito, a utilização de memória fica um pouco mais otimizada.

Dos métodos do componente, os mais importantes são:

  • public Boolean onUpdateByUser(BaseConcept concept, User userUpdating) – Verifica se um conceito está sendo editado por um determinado usuário.
  • public void releaseConceptsFromUser(User userUpdating) – Libera dos os conceitos em edição pelo usuário.
  • public void putOnUpdate(BaseConcept concept, User userUpdating) – “Trava” o conceito para edição por um determinado usuário.

Como vocês já devem ter percebido essa solução é intrusiva, ou seja, temos que codificar a chamada aos métodos pertinentes, na sequência apropriada para que tenhamos o comportamento desejado.

Portanto, a forma de utilização que adotei foi a seguinte:

  • Executo o método “releaseConceptsFromUser(User userUpdating)”:
    1. quando o usuário se autentica.
    2. quando o usuário acessa qualquer item de menu.
  • Quando o usuário solicita a edição de um registro:
    1. verifico se o conceito não está sendo editado usando o método “onUpdate(BaseConcept concept)”.
    2. caso o conceito esteja sendo editado por algum usuário eu devolvo uma mensagem de erro identificando o usuário que está fazendo a edição usando o método “identifyUpdater(BaseConcept concept)”.
    3. caso o conceito não esteja sendo editado eu carrego as informações, travo o conceito usando o método “putOnUpdate(BaseConcept concept, User userUpdating)” e encaminho o usuário para a página de edição dos dados.

Abaixo temos um exemplo de como usar o componente antes de carrgar a tela de alteração de um conceito:

public String loadUpdate(Material materialEdit) {
	material = entityManager.find(Material.class, materialEdit.getMaterialId());

	if (conceptOnUpdate.onUpdate(material)) {
		FacesMessages.instance().add(Severity.ERROR, "#{messages['concept.on.update.already.editing']}", conceptOnUpdate.identifyUpdater(material));
		return FW_LIST;
	} else {
		entityManager.refresh(material);
		conceptOnUpdate.putOnUpdate(material, userAuthenticated);
		return FW_EDIT;
	} //end if
} //end method

O ponto-forte dessa solução é que você escolhe como e quando usá-la o que é justamente o seu ponto-fraco, pois não é transparente a todos os implementadores e depende de uma orientação clara da forma como deve ser usada e quando deve ser usada. Mas, como disse, é uma solução que prima pela simplicidade de código e utilização.

Qualquer dúvida, por favor, deixe um comentário.