All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.component.jpa.JpaConsumer Maven / Gradle / Ivy

There is a newer version: 4.9.0
Show newest version
/**
 * 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 NOWAIT;
    private final EntityManagerFactory entityManagerFactory;
    private final TransactionTemplate transactionTemplate;
    private EntityManager entityManager;
    private QueryFactory queryFactory;
    private DeleteHandler deleteHandler;
    private DeleteHandler preDeleteHandler;
    private String query;
    private String namedQuery;
    private String nativeQuery;
    private LockModeType lockModeType = LockModeType.PESSIMISTIC_WRITE;
    private Map parameters;
    private Class resultClass;
    private boolean transacted;
    private boolean skipLockedEntity;

    static {
        NOWAIT = new HashMap();
        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 = transactionTemplate.execute(new TransactionCallback() {
            public Object doInTransaction(TransactionStatus status) {
                if (getEndpoint().isJoinTransaction()) {
                    entityManager.joinTransaction();
                }

                Queue answer = new LinkedList();

                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 {
                        // 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.info("Disposing EntityManager {} on {} due to coming transaction rollback", entityManager, this);
                        entityManager.close();
                        entityManager = null;
                        
                        // rollback all by throwning exception
                        throw cause;
                    }
                }

                // commit
                LOG.debug("Flushing EntityManager");
                entityManager.flush();
                // must clear after flush
                entityManager.clear();
                return messagePolled;
            }
        });

        return getEndpoint().getCamelContext().getTypeConverter().convertTo(int.class, messagePolled);
    }


    public int processBatch(Queue 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 was " + 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 getDeleteHandler() {
        if (deleteHandler == null) {
            deleteHandler = createDeleteHandler();
        }
        return deleteHandler;
    }

    public void setDeleteHandler(DeleteHandler deleteHandler) {
        this.deleteHandler = deleteHandler;
    }

    public DeleteHandler getPreDeleteHandler() {
        if (preDeleteHandler == null) {
            preDeleteHandler = createPreDeleteHandler();
        }
        return preDeleteHandler;
    }

    public void setPreDeleteHandler(DeleteHandler preDeleteHandler) {
        this.preDeleteHandler = preDeleteHandler;
    }

    public void setParameters(Map params) {
        this.parameters = params;
    }
    
    public Map 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.
     * 

* This option is default false. When false then all the good messages * will commit, and the first failed message will rollback. * However when true, 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 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 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() { @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() { @Override public void deleteObject(EntityManager entityManager, Object entityBean, Exchange exchange) { // Do nothing } }; } protected DeleteHandler createDeleteHandler() { // look for @Consumed to allow custom callback when the Entity has been consumed final Class entityType = getEndpoint().getEntityType(); if (entityType != null) { List 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() { 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() { public void deleteObject(EntityManager entityManager, Object entityBean, Exchange exchange) { entityManager.remove(entityBean); } }; } else { return new DeleteHandler() { 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 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(); } }