/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.runtime.core.routing; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.mule.runtime.api.metadata.DataType.STRING; import static org.mule.runtime.core.api.config.MuleProperties.OBJECT_STORE_MANAGER; import static org.mule.runtime.core.api.el.ExpressionManager.DEFAULT_EXPRESSION_POSTFIX; import static org.mule.runtime.core.api.el.ExpressionManager.DEFAULT_EXPRESSION_PREFIX; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.disposeIfNeeded; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.initialiseIfNeeded; import static org.slf4j.LoggerFactory.getLogger; import org.mule.runtime.api.el.BindingContext; import org.mule.runtime.api.exception.MuleException; import org.mule.runtime.api.lifecycle.Disposable; import org.mule.runtime.api.lifecycle.Initialisable; import org.mule.runtime.api.lifecycle.InitialisationException; import org.mule.runtime.api.meta.AbstractAnnotatedObject; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.api.construct.FlowConstruct; import org.mule.runtime.core.api.construct.FlowConstructAware; import org.mule.runtime.core.api.context.MuleContextAware; import org.mule.runtime.core.api.processor.Processor; import org.mule.runtime.core.api.routing.DuplicateMessageException; import org.mule.runtime.api.store.ObjectAlreadyExistsException; import org.mule.runtime.api.store.ObjectStore; import org.mule.runtime.api.store.ObjectStoreException; import org.mule.runtime.api.store.ObjectStoreManager; import org.mule.runtime.api.store.ObjectStoreNotAvailableException; import org.mule.runtime.core.exception.MessagingException; import org.slf4j.Logger; /** * <code>IdempotentMessageValidator</code> ensures that only unique messages are passed on. It does this by checking the unique ID * of the incoming message. Note that the underlying endpoint must support unique message IDs for this to work, otherwise a * <code>UniqueIdNotSupportedException</code> is thrown.<br> * <p> * <b>EIP Reference:</b> <a href="http://www.eaipatterns.com/IdempotentReceiver.html"> * http://www.eaipatterns.com/IdempotentReceiver.html</a> */ public class IdempotentMessageValidator extends AbstractAnnotatedObject implements Processor, MuleContextAware, FlowConstructAware, Initialisable, Disposable { private static final Logger LOGGER = getLogger(IdempotentMessageValidator.class); private static final BindingContext NULL_BINDING_CONTEXT = BindingContext.builder().build(); protected MuleContext muleContext; protected FlowConstruct flowConstruct; protected volatile ObjectStore<String> store; protected String storePrefix; protected String idExpression = format("%sid%s", DEFAULT_EXPRESSION_PREFIX, DEFAULT_EXPRESSION_POSTFIX); protected String valueExpression = format("%sid%s", DEFAULT_EXPRESSION_PREFIX, DEFAULT_EXPRESSION_POSTFIX); @Override public void setMuleContext(MuleContext context) { this.muleContext = context; } @Override public void setFlowConstruct(FlowConstruct flowConstruct) { this.flowConstruct = flowConstruct; } @Override public void initialise() throws InitialisationException { if (storePrefix == null) { storePrefix = format("%s.%s.%s", muleContext.getConfiguration().getId(), flowConstruct.getName(), this.getClass().getName()); } if (store == null) { this.store = createMessageIdStore(); } initialiseIfNeeded(store); } @Override public void dispose() { disposeIfNeeded(store, LOGGER); } protected ObjectStore<String> createMessageIdStore() throws InitialisationException { ObjectStoreManager objectStoreManager = muleContext.getRegistry().get(OBJECT_STORE_MANAGER); return objectStoreManager.getObjectStore(storePrefix, false, -1, MINUTES.toMillis(5), SECONDS.toMillis(6)); } protected String getValueForEvent(Event event) throws MessagingException { return (String) muleContext.getExpressionManager().evaluate(valueExpression, STRING, NULL_BINDING_CONTEXT, event).getValue(); } protected String getIdForEvent(Event event) throws MuleException { return (String) muleContext.getExpressionManager().evaluate(idExpression, STRING, NULL_BINDING_CONTEXT, event).getValue(); } public String getIdExpression() { return idExpression; } public void setIdExpression(String idExpression) { this.idExpression = idExpression; } public ObjectStore<String> getObjectStore() { return store; } public void setObjectStore(ObjectStore<String> store) { this.store = store; } private boolean accept(Event event) { if (event != null && isNewMessage(event)) { try { String id = getIdForEvent(event); String value = getValueForEvent(event); try { store.store(id, value); return true; } catch (ObjectAlreadyExistsException ex) { return false; } catch (ObjectStoreNotAvailableException e) { LOGGER.error("ObjectStore not available: " + e.getMessage()); return false; } catch (ObjectStoreException e) { LOGGER.warn("ObjectStore exception: " + e.getMessage()); return false; } } catch (MuleException e) { LOGGER.warn("Could not retrieve Id or Value for event: " + e.getMessage()); return false; } } else { return false; } } @Override public final Event process(Event event) throws MuleException { if (accept(event)) { return event; } else { throw new DuplicateMessageException(); } } protected boolean isNewMessage(Event event) { try { String id = this.getIdForEvent(event); if (store == null) { synchronized (this) { initialise(); } } return !store.contains(id); } catch (MuleException e) { LOGGER.error("Exception attempting to determine idempotency of incoming message for " + flowConstruct.getName() + " from the connector " + event.getContext().getOriginatingConnectorName(), e); return false; } } public String getValueExpression() { return valueExpression; } public void setValueExpression(String valueExpression) { this.valueExpression = valueExpression; } public void setStorePrefix(String storePrefix) { this.storePrefix = storePrefix; } }