/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.component.jpa; import java.lang.reflect.Method; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import javax.persistence.Entity; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.LockModeType; import javax.persistence.OptimisticLockException; import javax.persistence.PersistenceException; import javax.persistence.PessimisticLockException; import javax.persistence.Query; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.impl.ScheduledBatchPollingConsumer; import org.apache.camel.util.CastUtils; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.orm.jpa.SharedEntityManagerCreator; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; /** * @version */ public class JpaConsumer extends ScheduledBatchPollingConsumer { private static final Logger LOG = LoggerFactory.getLogger(JpaConsumer.class); private static final Map<String, Object> NOWAIT; private final EntityManagerFactory entityManagerFactory; private final TransactionTemplate transactionTemplate; private EntityManager entityManager; private QueryFactory queryFactory; private DeleteHandler<Object> deleteHandler; private DeleteHandler<Object> preDeleteHandler; private String query; private String namedQuery; private String nativeQuery; private LockModeType lockModeType = LockModeType.PESSIMISTIC_WRITE; private Map<String, Object> parameters; private Class<?> resultClass; private boolean transacted; private boolean skipLockedEntity; static { NOWAIT = new HashMap<String, Object>(); NOWAIT.put("javax.persistence.lock.timeout", 0L); } private static final class DataHolder { private Exchange exchange; private Object result; private EntityManager manager; private DataHolder() { } } public JpaConsumer(JpaEndpoint endpoint, Processor processor) { super(endpoint, processor); this.entityManagerFactory = endpoint.getEntityManagerFactory(); this.transactionTemplate = endpoint.createTransactionTemplate(); } @Override protected int poll() throws Exception { // must reset for each poll shutdownRunningTask = null; pendingExchanges = 0; // Recreate EntityManager in case it is disposed due to transaction rollback if (entityManager == null) { entityManager = entityManagerFactory.createEntityManager(); LOG.trace("Recreated EntityManager {} on {}", entityManager, this); } Object messagePolled = null; try { messagePolled = transactionTemplate.execute(new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus status) { if (getEndpoint().isJoinTransaction()) { entityManager.joinTransaction(); } Queue<DataHolder> answer = new LinkedList<DataHolder>(); Query query = getQueryFactory().createQuery(entityManager); configureParameters(query); LOG.trace("Created query {}", query); List<?> results = query.getResultList(); LOG.trace("Got result list from query {}", results); for (Object result : results) { DataHolder holder = new DataHolder(); holder.manager = entityManager; holder.result = result; holder.exchange = createExchange(result, entityManager); answer.add(holder); } PersistenceException cause = null; int messagePolled = 0; try { messagePolled = processBatch(CastUtils.cast(answer)); } catch (Exception e) { if (e instanceof PersistenceException) { cause = (PersistenceException) e; } else { cause = new PersistenceException(e); } } if (cause != null) { if (!isTransacted()) { LOG.warn("Error processing last message due: {}. Will commit all previous successful processed message, and ignore this last failure.", cause.getMessage(), cause); } else { // rollback all by throwning exception throw cause; } } // commit LOG.debug("Flushing EntityManager"); entityManager.flush(); // must clear after flush entityManager.clear(); return messagePolled; } }); } catch (Exception e) { // Potentially EntityManager could be in an inconsistent state after transaction rollback, // so disposing it to have it recreated in next poll. cf. Java Persistence API 3.3.2 Transaction Rollback LOG.debug("Disposing EntityManager {} on {} due to coming transaction rollback", entityManager, this); entityManager.close(); entityManager = null; throw new PersistenceException(e); } return getEndpoint().getCamelContext().getTypeConverter().convertTo(int.class, messagePolled); } public int processBatch(Queue<Object> exchanges) throws Exception { int total = exchanges.size(); // limit if needed if (maxMessagesPerPoll > 0 && total > maxMessagesPerPoll) { LOG.debug("Limiting to maximum messages to poll " + maxMessagesPerPoll + " as there were " + total + " messages in this poll."); total = maxMessagesPerPoll; } for (int index = 0; index < total && isBatchAllowed(); index++) { // only loop if we are started (allowed to run) DataHolder holder = ObjectHelper.cast(DataHolder.class, exchanges.poll()); EntityManager entityManager = holder.manager; Exchange exchange = holder.exchange; Object result = holder.result; // add current index and total as properties exchange.setProperty(Exchange.BATCH_INDEX, index); exchange.setProperty(Exchange.BATCH_SIZE, total); exchange.setProperty(Exchange.BATCH_COMPLETE, index == total - 1); // update pending number of exchanges pendingExchanges = total - index - 1; if (lockEntity(result, entityManager)) { // Run the @PreConsumed callback createPreDeleteHandler().deleteObject(entityManager, result, exchange); // process the current exchange LOG.debug("Processing exchange: {}", exchange); getProcessor().process(exchange); if (exchange.getException() != null) { // if we failed then throw exception throw exchange.getException(); } // Run the @Consumed callback getDeleteHandler().deleteObject(entityManager, result, exchange); } } return total; } // Properties // ------------------------------------------------------------------------- @Override public JpaEndpoint getEndpoint() { return (JpaEndpoint) super.getEndpoint(); } public QueryFactory getQueryFactory() { if (queryFactory == null) { queryFactory = createQueryFactory(); if (queryFactory == null) { throw new IllegalArgumentException("No queryType property configured on this consumer, nor an entityType configured on the endpoint so cannot consume"); } } return queryFactory; } public void setQueryFactory(QueryFactory queryFactory) { this.queryFactory = queryFactory; } public DeleteHandler<Object> getDeleteHandler() { if (deleteHandler == null) { deleteHandler = createDeleteHandler(); } return deleteHandler; } public void setDeleteHandler(DeleteHandler<Object> deleteHandler) { this.deleteHandler = deleteHandler; } public DeleteHandler<Object> getPreDeleteHandler() { if (preDeleteHandler == null) { preDeleteHandler = createPreDeleteHandler(); } return preDeleteHandler; } public void setPreDeleteHandler(DeleteHandler<Object> preDeleteHandler) { this.preDeleteHandler = preDeleteHandler; } public void setParameters(Map<String, Object> params) { this.parameters = params; } public Map<String, Object> getParameters() { return parameters; } public String getNamedQuery() { return namedQuery; } public void setNamedQuery(String namedQuery) { this.namedQuery = namedQuery; } public LockModeType getLockModeType() { return lockModeType; } public void setLockModeType(LockModeType lockModeType) { this.lockModeType = lockModeType; } public String getNativeQuery() { return nativeQuery; } public void setNativeQuery(String nativeQuery) { this.nativeQuery = nativeQuery; } public String getQuery() { return query; } public void setQuery(String query) { this.query = query; } public Class<?> getResultClass() { return resultClass; } public void setResultClass(Class<?> resultClass) { this.resultClass = resultClass; } public boolean isTransacted() { return transacted; } /** * Sets whether to run in transacted mode or not. * <p/> * This option is default <tt>false</tt>. When <tt>false</tt> then all the good messages * will commit, and the first failed message will rollback. * However when <tt>true</tt>, then all messages will rollback, if just one message failed. */ public void setTransacted(boolean transacted) { this.transacted = transacted; } /** * Sets whether to use NOWAIT on lock and silently skip the entity. This * allows different instances to process entities at the same time but not * processing the same entity. */ public void setSkipLockedEntity(boolean skipLockedEntity) { this.skipLockedEntity = skipLockedEntity; } public boolean isSkipLockedEntity() { return skipLockedEntity; } // Implementation methods // ------------------------------------------------------------------------- /** * A strategy method to lock an object with an exclusive lock so that it can * be processed * * @param entity the entity to be locked * @param entityManager entity manager * @return true if the entity was locked */ protected boolean lockEntity(Object entity, EntityManager entityManager) { if (!getEndpoint().isConsumeLockEntity()) { return true; } try { LOG.debug("Acquiring exclusive lock on entity: {}", entity); if (isSkipLockedEntity()) { entityManager.lock(entity, lockModeType, NOWAIT); } else { entityManager.lock(entity, lockModeType); } return true; } catch (Exception e) { if (LOG.isDebugEnabled()) { LOG.debug("Failed to achieve lock on entity: " + entity + ". Reason: " + e, e); } if (e instanceof PessimisticLockException || e instanceof OptimisticLockException) { //transaction marked as rollback can't continue gracefully throw (PersistenceException) e; } //TODO: Find if possible an alternative way to handle results of native queries. //Result of native queries are Arrays and cannot be locked by all JPA Providers. if (entity.getClass().isArray()) { return true; } return false; } } protected QueryFactory createQueryFactory() { if (query != null) { return QueryBuilder.query(query); } else if (namedQuery != null) { return QueryBuilder.namedQuery(namedQuery); } else if (nativeQuery != null) { if (resultClass != null) { return QueryBuilder.nativeQuery(nativeQuery, resultClass); } else { return QueryBuilder.nativeQuery(nativeQuery); } } else { Class<?> entityType = getEndpoint().getEntityType(); if (entityType == null) { return null; } else { // Check if we have a property name on the @Entity annotation String name = getEntityName(entityType); if (name != null) { return QueryBuilder.query("select x from " + name + " x"); } else { // Remove package name of the entity to be conform with JPA 1.0 spec return QueryBuilder.query("select x from " + entityType.getSimpleName() + " x"); } } } } protected String getEntityName(Class<?> clazz) { Entity entity = clazz.getAnnotation(Entity.class); // Check if the property name has been defined for Entity annotation if (entity != null && !entity.name().equals("")) { return entity.name(); } else { return null; } } protected DeleteHandler<Object> createPreDeleteHandler() { // Look for @PreConsumed to allow custom callback before the Entity has been consumed final Class<?> entityType = getEndpoint().getEntityType(); if (entityType != null) { // Inspect the method(s) annotated with @PreConsumed List<Method> methods = ObjectHelper.findMethodsWithAnnotation(entityType, PreConsumed.class); if (methods.size() > 1) { throw new IllegalStateException("Only one method can be annotated with the @PreConsumed annotation but found: " + methods); } else if (methods.size() == 1) { // Inspect the parameters of the @PreConsumed method final Method method = methods.get(0); final boolean useExchangeParameter = checkParameters(method); return new DeleteHandler<Object>() { @Override public void deleteObject(EntityManager entityManager, Object entityBean, Exchange exchange) { // The entityBean could be an Object array if (entityType.isInstance(entityBean)) { if (useExchangeParameter) { ObjectHelper.invokeMethod(method, entityBean, exchange); } else { ObjectHelper.invokeMethod(method, entityBean); } } } }; } } // else do nothing return new DeleteHandler<Object>() { @Override public void deleteObject(EntityManager entityManager, Object entityBean, Exchange exchange) { // Do nothing } }; } protected DeleteHandler<Object> createDeleteHandler() { // look for @Consumed to allow custom callback when the Entity has been consumed final Class<?> entityType = getEndpoint().getEntityType(); if (entityType != null) { List<Method> methods = ObjectHelper.findMethodsWithAnnotation(entityType, Consumed.class); if (methods.size() > 1) { throw new IllegalArgumentException("Only one method can be annotated with the @Consumed annotation but found: " + methods); } else if (methods.size() == 1) { final Method method = methods.get(0); final boolean useExchangeParameter = checkParameters(method); return new DeleteHandler<Object>() { public void deleteObject(EntityManager entityManager, Object entityBean, Exchange exchange) { if (entityType.isInstance(entityBean)) { if (useExchangeParameter) { ObjectHelper.invokeMethod(method, entityBean, exchange); } else { ObjectHelper.invokeMethod(method, entityBean); } } } }; } } if (getEndpoint().isConsumeDelete()) { return new DeleteHandler<Object>() { public void deleteObject(EntityManager entityManager, Object entityBean, Exchange exchange) { entityManager.remove(entityBean); } }; } else { return new DeleteHandler<Object>() { public void deleteObject(EntityManager entityManager, Object entityBean, Exchange exchange) { // do nothing } }; } } protected boolean checkParameters(Method method) { boolean result = false; Class<?>[] parameters = method.getParameterTypes(); if (parameters.length == 1 && parameters[0].isAssignableFrom(Exchange.class)) { result = true; } if (parameters.length > 0 && !result) { throw new IllegalStateException("@PreConsumed annotated method cannot have parameter other than Exchange"); } return result; } protected void configureParameters(Query query) { int maxResults = getEndpoint().getMaximumResults(); if (maxResults > 0) { query.setMaxResults(maxResults); } // setup the parameter if (parameters != null) { for (Entry<String, Object> entry : parameters.entrySet()) { query.setParameter(entry.getKey(), entry.getValue()); } } } protected Exchange createExchange(Object result, EntityManager entityManager) { Exchange exchange = getEndpoint().createExchange(); exchange.getIn().setBody(result); exchange.getIn().setHeader(JpaConstants.ENTITY_MANAGER, entityManager); return exchange; } @Override protected void doStart() throws Exception { // need to setup entity manager first if (getEndpoint().isSharedEntityManager()) { this.entityManager = SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); } else { this.entityManager = entityManagerFactory.createEntityManager(); } LOG.trace("Created EntityManager {} on {}", entityManager, this); super.doStart(); } @Override protected void doStop() throws Exception { // noop } @Override protected void doShutdown() throws Exception { if (entityManager != null) { this.entityManager.close(); LOG.trace("Closed EntityManager {} on {}", entityManager, this); } super.doShutdown(); } }