/** * Copyright 2010 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jbpm.process.audit; import java.util.List; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.TransactionRequiredException; import javax.transaction.NotSupportedException; import javax.transaction.Status; import javax.transaction.SystemException; import javax.transaction.UserTransaction; import org.drools.core.WorkingMemory; import org.drools.core.common.InternalWorkingMemory; import org.drools.core.runtime.process.InternalProcessRuntime; import org.drools.persistence.api.TransactionManager; import org.jbpm.process.audit.variable.ProcessIndexerManager; import org.jbpm.process.instance.impl.ProcessInstanceImpl; import org.jbpm.workflow.instance.impl.NodeInstanceImpl; import org.kie.api.event.KieRuntimeEvent; import org.kie.api.event.process.ProcessCompletedEvent; import org.kie.api.event.process.ProcessEventListener; import org.kie.api.event.process.ProcessNodeLeftEvent; import org.kie.api.event.process.ProcessNodeTriggeredEvent; import org.kie.api.event.process.ProcessStartedEvent; import org.kie.api.event.process.ProcessVariableChangedEvent; import org.kie.api.runtime.Environment; import org.kie.api.runtime.EnvironmentName; import org.kie.api.runtime.KieSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Enables history log via JPA. * */ public class JPAWorkingMemoryDbLogger extends AbstractAuditLogger { private static final Logger logger = LoggerFactory.getLogger(JPAWorkingMemoryDbLogger.class); private static final String[] KNOWN_UT_JNDI_KEYS = new String[] {"UserTransaction", "java:jboss/UserTransaction", System.getProperty("jbpm.ut.jndi.lookup")}; private boolean isJTA = true; private boolean sharedEM = false; private EntityManagerFactory emf; private ProcessIndexerManager indexManager = ProcessIndexerManager.get(); /* * for backward compatibility */ public JPAWorkingMemoryDbLogger(WorkingMemory workingMemory) { super(workingMemory); InternalProcessRuntime processRuntime = ((InternalWorkingMemory) workingMemory).getProcessRuntime(); if (processRuntime != null) { processRuntime.addEventListener( (ProcessEventListener) this ); } } public JPAWorkingMemoryDbLogger(KieSession session) { Environment env = session.getEnvironment(); internalSetIsJTA(env); session.addEventListener(this); } /* * end of backward compatibility */ public JPAWorkingMemoryDbLogger(EntityManagerFactory emf) { this.emf = emf; } public JPAWorkingMemoryDbLogger() { // default constructor when this is used with a persistent KieSession } public JPAWorkingMemoryDbLogger(EntityManagerFactory emf, Environment env) { this.emf = emf; internalSetIsJTA(env); } public JPAWorkingMemoryDbLogger(Environment env) { internalSetIsJTA(env); } private void internalSetIsJTA(Environment env) { Boolean bool = (Boolean) env.get("IS_JTA_TRANSACTION"); if (bool != null) { isJTA = bool.booleanValue(); } } @Override public void beforeNodeTriggered(ProcessNodeTriggeredEvent event) { NodeInstanceLog log = (NodeInstanceLog) builder.buildEvent(event); persist(log, event); ((NodeInstanceImpl) event.getNodeInstance()).getMetaData().put("NodeInstanceLog", log); } @Override public void afterNodeLeft(ProcessNodeLeftEvent event) { NodeInstanceLog log = (NodeInstanceLog) builder.buildEvent(event, null); persist(log, event); } @Override public void afterVariableChanged(ProcessVariableChangedEvent event) { List<org.kie.api.runtime.manager.audit.VariableInstanceLog> variables = indexManager.index(getBuilder(), event); for (org.kie.api.runtime.manager.audit.VariableInstanceLog log : variables) { persist(log, event); } } @Override public void beforeProcessStarted(ProcessStartedEvent event) { ProcessInstanceLog log = (ProcessInstanceLog) builder.buildEvent(event); persist(log, event); ((ProcessInstanceImpl) event.getProcessInstance()).getMetaData().put("ProcessInstanceLog", log); } @Override public void afterProcessCompleted(ProcessCompletedEvent event) { long processInstanceId = event.getProcessInstance().getId(); EntityManager em = getEntityManager(event); Object tx = joinTransaction(em); ProcessInstanceLog log = (ProcessInstanceLog) ((ProcessInstanceImpl) event.getProcessInstance()).getMetaData().get("ProcessInstanceLog"); if (log == null) { List<ProcessInstanceLog> result = em.createQuery( "from ProcessInstanceLog as log where log.processInstanceId = :piId and log.end is null") .setParameter("piId", processInstanceId).getResultList(); if (result != null && result.size() != 0) { log = result.get(result.size() - 1); } } if (log != null) { log = (ProcessInstanceLog) builder.buildEvent(event, log); em.merge(log); } leaveTransaction(em, tx); } @Override public void afterNodeTriggered(ProcessNodeTriggeredEvent event) { // trigger this to record some of the data (like work item id) after activity was triggered NodeInstanceLog log = (NodeInstanceLog) ((NodeInstanceImpl) event.getNodeInstance()).getMetaData().get("NodeInstanceLog"); builder.buildEvent(event, log); } @Override public void beforeNodeLeft(ProcessNodeLeftEvent event) { } @Override public void beforeVariableChanged(ProcessVariableChangedEvent event) { } @Override public void afterProcessStarted(ProcessStartedEvent event) { } @Override public void beforeProcessCompleted(ProcessCompletedEvent event) { } public void dispose() { } /** * This method persists the entity given to it. * </p> * This method also makes sure that the entity manager used for persisting the entity, joins the existing JTA transaction. * @param entity An entity to be persisted. */ private void persist(Object entity, KieRuntimeEvent event) { EntityManager em = getEntityManager(event); Object tx = joinTransaction(em); em.persist(entity); leaveTransaction(em, tx); } /** * This method creates a entity manager. */ private EntityManager getEntityManager(KieRuntimeEvent event) { Environment env = event.getKieRuntime().getEnvironment(); /** * It's important to set the sharedEM flag with _every_ operation * otherwise, there are situations where: * 1. it can be set to "true" * 2. something can happen * 3. the "true" value can no longer apply * (I've seen this in debugging logs.. ) */ sharedEM = false; if( emf != null ) { return emf.createEntityManager(); } else if (env != null) { EntityManagerFactory emf = (EntityManagerFactory) env.get(EnvironmentName.ENTITY_MANAGER_FACTORY); // first check active transaction if it contains entity manager EntityManager em = getEntityManagerFromTransaction(env); if (em != null && em.isOpen() && em.getEntityManagerFactory().equals(emf)) { sharedEM = true; return em; } // next check the environment itself em = (EntityManager) env.get(EnvironmentName.CMD_SCOPED_ENTITY_MANAGER); if (em != null) { sharedEM = true; return em; } // lastly use entity manager factory if (emf != null) { return emf.createEntityManager(); } } throw new RuntimeException("Could not find or create a new EntityManager!"); } protected EntityManager getEntityManagerFromTransaction(Environment env) { if (env.get(EnvironmentName.TRANSACTION_MANAGER) instanceof TransactionManager) { TransactionManager txm = (TransactionManager) env.get(EnvironmentName.TRANSACTION_MANAGER); EntityManager em = (EntityManager) txm.getResource(EnvironmentName.CMD_SCOPED_ENTITY_MANAGER); return em; } return null; } /** * This method opens a new transaction, if none is currently running, and joins the entity manager/persistence context * to that transaction. * @param em The entity manager we're using. * @return {@link UserTransaction} If we've started a new transaction, then we return it so that it can be closed. * @throws NotSupportedException * @throws SystemException * @throws Exception if something goes wrong. */ private Object joinTransaction(EntityManager em) { boolean newTx = false; UserTransaction ut = null; if (isJTA) { try { em.joinTransaction(); } catch (TransactionRequiredException e) { ut = findUserTransaction(); try { if( ut != null && ut.getStatus() == Status.STATUS_NO_TRANSACTION ) { ut.begin(); newTx = true; // since new transaction was started em must join it em.joinTransaction(); } } catch(Exception ex) { throw new IllegalStateException("Unable to find or open a transaction: " + ex.getMessage(), ex); } if (!newTx) { // rethrow TransactionRequiredException if UserTransaction was not found or started throw e; } } if( newTx ) { return ut; } } // else { // EntityTransaction tx = em.getTransaction(); // if( ! tx.isActive() ) { // tx.begin(); // return tx; // } // } return null; } /** * This method closes the entity manager and transaction. It also makes sure that any objects associated * with the entity manager/persistence context are detached. * </p> * Obviously, if the transaction returned by the {@link #joinTransaction(EntityManager)} method is null, * nothing is done with the transaction parameter. * @param em The entity manager. * @param ut The (user) transaction. */ private void leaveTransaction(EntityManager em, Object transaction) { if( isJTA ) { try { if( transaction != null ) { // There's a tx running, close it. ((UserTransaction) transaction).commit(); } } catch(Exception e) { logger.error("Unable to commit transaction: ", e); } } else { if( transaction != null ) { ((EntityTransaction) transaction).commit(); } } if (!sharedEM) { try { em.flush(); em.close(); } catch( Exception e ) { logger.error("Unable to close created EntityManager: {}", e.getMessage(), e); } } } protected static UserTransaction findUserTransaction() { InitialContext context = null; try { context = new InitialContext(); return (UserTransaction) context.lookup( "java:comp/UserTransaction" ); } catch ( NamingException ex ) { for (String utLookup : KNOWN_UT_JNDI_KEYS) { if (utLookup != null) { try { UserTransaction ut = (UserTransaction) context.lookup(utLookup); return ut; } catch (NamingException e) { logger.debug("User Transaction not found in JNDI under {}", utLookup); } } } logger.warn("No user transaction found under known names"); return null; } } }