/*
* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.mediator.cache;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.cache.CacheBuilder;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.clustering.ClusteringFault;
import org.apache.axis2.clustering.state.Replicator;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.OperationContext;
import org.apache.synapse.ManagedLifecycle;
import org.apache.synapse.Mediator;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseException;
import org.apache.synapse.SynapseLog;
import org.apache.synapse.config.SynapseConfiguration;
import org.apache.synapse.continuation.ContinuationStackManager;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.apache.synapse.core.axis2.Axis2Sender;
import org.apache.synapse.debug.constructs.EnclosedInlinedSequence;
import org.apache.synapse.mediators.AbstractMediator;
import org.apache.synapse.mediators.base.SequenceMediator;
import org.apache.synapse.util.FixedByteArrayOutputStream;
import org.apache.synapse.util.MessageHelper;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.mediator.cache.digest.DigestGenerator;
import org.wso2.carbon.mediator.cache.util.RequestHash;
import org.wso2.carbon.mediator.cache.util.SOAPMessageHelper;
import javax.cache.Cache;
import javax.cache.CacheConfiguration;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.MalformedObjectNameException;
import javax.management.InstanceAlreadyExistsException;
import javax.management.NotCompliantMBeanException;
import javax.management.MBeanRegistrationException;
import javax.xml.soap.SOAPException;
import javax.xml.stream.XMLStreamException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* CacheMediator will cache the response messages indexed using the hash value of the request message,
* and subsequent messages with the same request (request hash will be generated and checked for the equality) within
* the cache expiration period will be served from the stored responses in the cache
*
* @see org.apache.synapse.Mediator
*/
public class CacheMediator extends AbstractMediator implements ManagedLifecycle, EnclosedInlinedSequence {
/**
* Cache configuration ID.
*/
private String id = null;
/**
* The scope of the cache
*/
private String scope = CachingConstants.SCOPE_PER_HOST;
/**
* This specifies whether the mediator should be in the incoming path (to check the request) or in the outgoing
* path (to cache the response).
*/
private boolean collector = false;
/**
* This is used to define the logic used by the mediator to evaluate the hash values of incoming messages.
*/
private DigestGenerator digestGenerator = CachingConstants.DEFAULT_XML_IDENTIFIER;
/**
* The size of the messages to be cached in memory. If this is 0 then no disk cache,
* and if there is no size specified in the config factory will asign a default value to enable disk based caching.
*/
private int inMemoryCacheSize = CachingConstants.DEFAULT_CACHE_SIZE;
/**
* The size of the messages to be cached in memory. Disk based and hirearchycal caching is not implemented yet.
*/
private int diskCacheSize = 0;
/**
* The time duration for which the cache is kept.
*/
private long timeout = 0L;
/**
* The SequenceMediator to the onCacheHit sequence to be executed when an incoming message is identified as an
* equivalent to a previously received message based on the value defined for the Hash Generator field.
*/
private SequenceMediator onCacheHitSequence = null;
/**
* The reference to the onCacheHit sequence to be executed when an incoming message is identified as an
* equivalent to a previously received message based on the value defined for the Hash Generator field.
*/
private String onCacheHitRef = null;
/**
* The maximum size of the messages to be cached. This is specified in bytes.
*/
private int maxMessageSize = 0;
/**
* Prefix of the cache key
*/
private static final String CACHE_KEY_PREFIX = "mediation.cache_key_";
/**
* Key to use in cache configuration
*/
private String cacheKey = "mediation.cache_key";
/**
* This holds whether the global cache already initialized or not.
*/
private static AtomicBoolean mediatorCacheInit = new AtomicBoolean(false);
@Override
public void init(SynapseEnvironment se) {
if (onCacheHitSequence != null) {
onCacheHitSequence.init(se);
}
exposeData(se.createMessageContext());
}
@Override
public void destroy() {
if (onCacheHitSequence != null) {
onCacheHitSequence.destroy();
}
}
@Override
public boolean isContentAware() {
return true;
}
@Override
public boolean mediate(MessageContext synCtx) {
if (synCtx.getEnvironment().isDebuggerEnabled()) {
if (super.divertMediationRoute(synCtx)) {
return true;
}
}
SynapseLog synLog = getLog(synCtx);
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Start : Cache mediator");
if (synLog.isTraceTraceEnabled()) {
synLog.traceTrace("Message : " + synCtx.getEnvelope());
}
}
// if maxMessageSize is specified check for the message size before processing
if (maxMessageSize > 0) {
FixedByteArrayOutputStream fbaos = new FixedByteArrayOutputStream(maxMessageSize);
try {
MessageHelper.cloneSOAPEnvelope(synCtx.getEnvelope()).serialize(fbaos);
} catch (XMLStreamException e) {
handleException("Error in checking the message size", e, synCtx);
} catch (SynapseException syne) {
synLog.traceOrDebug("Message size exceeds the upper bound for caching, request will not be cached");
return true;
} finally {
try {
fbaos.close();
} catch (IOException e) {
handleException("Error occurred while closing the FixedByteArrayOutputStream ", e, synCtx);
}
}
}
ConfigurationContext cfgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext().getConfigurationContext();
if (cfgCtx == null) {
handleException("Unable to perform caching, ConfigurationContext cannot be found", synCtx);
return false; // never executes.. but keeps IDE happy
}
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Looking up cache at scope : " + scope + " with ID : " + cacheKey);
}
boolean result = true;
try {
if (synCtx.isResponse()) {
processResponseMessage(synCtx, cfgCtx, synLog);
} else {
result = processRequestMessage(synCtx, synLog);
}
} catch (ClusteringFault clusteringFault) {
synLog.traceOrDebug("Unable to replicate Cache mediator state among the cluster");
}
synLog.traceOrDebug("End : Cache mediator");
return result;
}
/**
* Process a response message through this cache mediator. This finds the Cache used, and
* updates it for the corresponding request hash
*
* @param synLog the Synapse log to use
* @param synCtx the current message (response)
* @param cfgCtx the abstract context in which the cache will be kept
* @throws ClusteringFault is there is an error in replicating the cfgCtx
*/
private void processResponseMessage(MessageContext synCtx, ConfigurationContext cfgCtx,
SynapseLog synLog) throws ClusteringFault {
if (!collector) {
handleException("Response messages cannot be handled in a non collector cache", synCtx);
}
org.apache.axis2.context.MessageContext msgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext();
OperationContext operationContext = msgCtx.getOperationContext();
CachableResponse response = (CachableResponse) operationContext.getProperty(CachingConstants.CACHED_OBJECT);
if (response != null) {
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Storing the response message into the cache at scope : " + scope + " with ID : "
+ cacheKey + " for request hash : " + response.getRequestHash());
}
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Storing the response for the message with ID : " + synCtx.getMessageID() + " " +
"with request hash ID : " + response.getRequestHash() + " in the cache : " +
cacheKey);
}
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try {
synCtx.getEnvelope().serialize(outStream);
response.setResponseEnvelope(outStream.toByteArray());
if (msgCtx.isDoingREST()) {
response.setSOAP11(synCtx.isSOAP11());
Map<String, String> headers =
(Map) msgCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
String messageType = (String) msgCtx.getProperty(Constants.Configuration.MESSAGE_TYPE);
Map<String, Object> headerProperties = new HashMap<String, Object>();
//Individually copying All TRANSPORT_HEADERS to headerProperties Map instead putting whole
//TRANSPORT_HEADERS map as single Key/Value pair to fix hazelcast serialization issue.
for (Map.Entry<String, String> entry : headers.entrySet()) {
headerProperties.put(entry.getKey(), entry.getValue());
}
headerProperties.put(Constants.Configuration.MESSAGE_TYPE, messageType);
response.setHeaderProperties(headerProperties);
}
} catch (XMLStreamException e) {
handleException("Unable to set the response to the Cache", e, synCtx);
} finally {
try {
outStream.close();
} catch (IOException e) {
handleException("Error occurred while closing the FixedByteArrayOutputStream ", e, synCtx);
}
}
if (response.getTimeout() > 0) {
response.setExpireTimeMillis(System.currentTimeMillis() + response.getTimeout());
}
getMediatorCache().put(response.getRequestHash(), response);
// Finally, we may need to replicate the changes in the cache
Replicator.replicate(cfgCtx);
} else {
synLog.auditWarn("A response message without a valid mapping to the " +
"request hash found. Unable to store the response in cache");
}
}
/**
* Processes a request message through the cache mediator. Generates the request hash and looks
* up for a hit, if found; then the specified named or anonymous sequence is executed or marks
* this message as a response and sends back directly to client.
*
* @param synCtx incoming request message
* @param synLog the Synapse log to use
* @return should this mediator terminate further processing?
* @throws ClusteringFault if there is an error in replicating the cfgCtx
*/
private boolean processRequestMessage(MessageContext synCtx,
SynapseLog synLog) throws ClusteringFault {
if (collector) {
handleException("Request messages cannot be handled in a collector cache", synCtx);
}
OperationContext opCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext().getOperationContext();
String requestHash = null;
try {
requestHash = digestGenerator.getDigest(((Axis2MessageContext) synCtx).getAxis2MessageContext());
synCtx.setProperty(CachingConstants.REQUEST_HASH, requestHash);
} catch (CachingException e) {
handleException("Error in calculating the hash value of the request", e, synCtx);
}
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Generated request hash : " + requestHash);
}
RequestHash hash = new RequestHash(requestHash);
CachableResponse cachedResponse = getMediatorCache().get(requestHash);
org.apache.axis2.context.MessageContext msgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext();
opCtx.setProperty(CachingConstants.REQUEST_HASH, requestHash);
byte[] responseEnvelop;
Map<String, Object> headerProperties;
if (cachedResponse != null && (responseEnvelop = cachedResponse.getResponseEnvelope()) != null) {
// get the response from the cache and attach to the context and change the
// direction of the message
if (!cachedResponse.isExpired()) {
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Cache-hit for message ID : " + synCtx.getMessageID());
}
cachedResponse.setInUse(true);
// mark as a response and replace envelope from cache
synCtx.setResponse(true);
opCtx.setProperty(CachingConstants.CACHED_OBJECT, cachedResponse);
SOAPEnvelope omSOAPEnv = null;
try {
if (msgCtx.isDoingREST()) {
if ((headerProperties = cachedResponse.getHeaderProperties()) != null) {
omSOAPEnv = SOAPMessageHelper.buildSOAPEnvelopeFromBytes(responseEnvelop, cachedResponse.isSOAP11());
msgCtx.removeProperty("NO_ENTITY_BODY");
msgCtx.removeProperty(Constants.Configuration.CONTENT_TYPE);
msgCtx.setProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS, headerProperties);
msgCtx.setProperty(Constants.Configuration.MESSAGE_TYPE,
headerProperties.get(Constants.Configuration.MESSAGE_TYPE));
}
} else {
omSOAPEnv = SOAPMessageHelper.buildSOAPEnvelopeFromBytes(responseEnvelop, msgCtx.isSOAP11());
//finally set soap envelope by obtaining built response envelope
cachedResponse.setResponseEnvelope(omSOAPEnv.toString().getBytes());
}
if (omSOAPEnv != null) {
synCtx.setEnvelope(omSOAPEnv);
}
} catch (AxisFault axisFault) {
handleException("Error setting response envelope from cache : "
+ cacheKey, synCtx);
} catch (IOException ioe) {
handleException("Error setting response envelope from cache : "
+ cacheKey, ioe, synCtx);
} catch (SOAPException soape) {
handleException("Error setting response envelope from cache : "
+ cacheKey, soape, synCtx);
}
// take specified action on cache hit
if (onCacheHitSequence != null) {
// if there is an onCacheHit use that for the mediation
synLog.traceOrDebug("Delegating message to the onCachingHit "
+ "Anonymous sequence");
ContinuationStackManager.addReliantContinuationState(synCtx, 0, getMediatorPosition());
if (onCacheHitSequence.mediate(synCtx)) {
ContinuationStackManager.removeReliantContinuationState(synCtx);
}
} else if (onCacheHitRef != null) {
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Delegating message to the onCachingHit " +
"sequence : " + onCacheHitRef);
}
ContinuationStackManager.updateSeqContinuationState(synCtx, getMediatorPosition());
synCtx.getSequence(onCacheHitRef).mediate(synCtx);
} else {
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Request message " + synCtx.getMessageID() +
" was served from the cache : " + cacheKey);
}
// send the response back if there is not onCacheHit is specified
synCtx.setTo(null);
Axis2Sender.sendBack(synCtx);
}
// stop any following mediators from executing
return false;
} else {
cachedResponse.reincarnate(timeout);
if (synLog.isTraceOrDebugEnabled()) {
synLog.traceOrDebug("Existing cached response has expired. Resetting cache element");
}
getMediatorCache().put(hash.getRequestHash(), cachedResponse);
opCtx.setProperty(CachingConstants.CACHED_OBJECT, cachedResponse);
Replicator.replicate(opCtx);
}
} else {
cacheNewResponse(msgCtx, hash);
}
return true;
}
/**
* Caches the CachableResponse object with currently available attributes against the requestHash in Cache<String,
* CachableResponse>
*
* @param msgContext axis2 message context of the request message
* @param requestHash the request hash that has already been computed
* @throws ClusteringFault if there is an error in replicating the cfgCtx
*/
private void cacheNewResponse(org.apache.axis2.context.MessageContext msgContext, RequestHash requestHash)
throws ClusteringFault {
OperationContext opCtx = msgContext.getOperationContext();
CachableResponse response = new CachableResponse();
response.setRequestHash(requestHash.getRequestHash());
response.setTimeout(timeout);
getMediatorCache().put(requestHash.getRequestHash(), response);
opCtx.setProperty(CachingConstants.CACHED_OBJECT, response);
Replicator.replicate(opCtx);
}
/**
* Exposes the whole mediator cache through jmx MBean
*
* @param msgCtx cache response msgCtx
*/
public void exposeData(MessageContext msgCtx) {
String serverPackage = "org.wso2.carbon.mediation";
String objectName = serverPackage + ":type=Cache,tenant=" + PrivilegedCarbonContext
.getThreadLocalCarbonContext().getTenantDomain() + ",manager=" + Caching.getCacheManagerFactory().
getCacheManager(CachingConstants.CACHE_MANAGER).getName() + ",name=" + getMediatorCache().getName();
try {
MBeanServer mserver = getMBeanServer();
ObjectName cacheMBeanObjName = new ObjectName(objectName);
Set<ObjectName> set = mserver.queryNames(new ObjectName(objectName), null);
if (set.isEmpty()) {
MediatorCacheInvalidator cacheMBean = new MediatorCacheInvalidator(
PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(),
PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(), msgCtx);
mserver.registerMBean(cacheMBean, cacheMBeanObjName);
}
} catch (MalformedObjectNameException e) {
handleException("The format of the string does not correspond to a valid ObjectName.", e, msgCtx);
} catch (InstanceAlreadyExistsException e) {
handleException("MBean with the name " + objectName + " is already registered.", e, msgCtx);
} catch (NotCompliantMBeanException e) {
handleException("MBean implementation is not compliant with JMX specification standard MBean.", e, msgCtx);
} catch (MBeanRegistrationException e) {
handleException("Could not register MediatorCacheInvalidator MBean.", e, msgCtx);
}
}
/**
* Obtains existing mbean server instance or create new one
*
* @return MBeanServer instance
*/
private MBeanServer getMBeanServer() {
MBeanServer mserver;
if (MBeanServerFactory.findMBeanServer(null).size() > 0) {
mserver = MBeanServerFactory.findMBeanServer(null).get(0);
} else {
mserver = MBeanServerFactory.createMBeanServer();
}
return mserver;
}
/**
* Creates default cache to keep mediator cache
*
* @return global cache
*/
public static Cache<String, CachableResponse> getMediatorCache() {
if (mediatorCacheInit.get()) {
return Caching.getCacheManagerFactory().getCacheManager(CachingConstants.CACHE_MANAGER)
.getCache(CachingConstants.MEDIATOR_CACHE);
} else {
CacheManager cacheManager = Caching.getCacheManagerFactory().getCacheManager(CachingConstants.CACHE_MANAGER);
mediatorCacheInit.getAndSet(true);
CacheBuilder<String, CachableResponse> mediatorCacheBuilder = cacheManager.createCacheBuilder(CachingConstants.MEDIATOR_CACHE);
Cache<String, CachableResponse> cache = mediatorCacheBuilder.setExpiry(CacheConfiguration.ExpiryType.MODIFIED,
new CacheConfiguration.Duration(TimeUnit.SECONDS, CachingConstants.CACHE_INVALIDATION_TIME))
.setExpiry(CacheConfiguration.ExpiryType.ACCESSED,
new CacheConfiguration.Duration(TimeUnit.SECONDS, CachingConstants.CACHE_INVALIDATION_TIME))
.setStoreByValue(false).build();
return cache;
}
}
/**
* This methods gives the ID of the cache configuration.
*
* @return string cache configuration ID.
*/
public String getId() {
return id;
}
/**
* This methods sets the ID of the cache configuration.
*
* @param id cache configuration ID to be set.
*/
public void setId(String id) {
this.id = id;
}
/**
* This method gives the scope of the cache.
*
* @return value of the cache scope.
*/
public String getScope() {
return scope;
}
/**
* This method sets the scope of the cache.
*
* @param scope cache scope to be set.
*/
public void setScope(String scope) {
this.scope = scope;
if (CachingConstants.SCOPE_PER_MEDIATOR.equals(scope)) {
cacheKey = CACHE_KEY_PREFIX + id;
}
}
/**
* This method gives whether the mediator should be in the incoming path or in the outgoing path as a boolean.
*
* @return boolean true if incoming path false if outgoing path.
*/
public boolean isCollector() {
return collector;
}
/**
* This method sets whether the mediator should be in the incoming path or in the outgoing path as a boolean.
*
* @param collector boolean value to be set as collector.
*/
public void setCollector(boolean collector) {
this.collector = collector;
}
/**
* This method gives the DigestGenerator to evaluate the hash values of incoming messages.
*
* @return DigestGenerator used evaluate hash values.
*/
public DigestGenerator getDigestGenerator() {
return digestGenerator;
}
/**
* This method sets the DigestGenerator to evaluate the hash values of incoming messages.
*
* @param digestGenerator DigestGenerator to be set to evaluate hash values.
*/
public void setDigestGenerator(DigestGenerator digestGenerator) {
this.digestGenerator = digestGenerator;
}
/**
* This method gives the size of the messages to be cached in memory.
*
* @return memory cache size in bytes.
*/
public int getInMemoryCacheSize() {
return inMemoryCacheSize;
}
/**
* This method sets the size of the messages to be cached in memory.
*
* @param inMemoryCacheSize value(number of bytes) to be set as memory cache size.
*/
public void setInMemoryCacheSize(int inMemoryCacheSize) {
this.inMemoryCacheSize = inMemoryCacheSize;
}
/**
* This method gives the size of the messages to be cached in disk.
*
* @return disk cache size in bytes.
*/
public int getDiskCacheSize() {
return diskCacheSize;
}
/**
* This method sets the size of the messages to be cached in disk.
*
* @param diskCacheSize value(number of bytes) to be set as disk cache size.
*/
public void setDiskCacheSize(int diskCacheSize) {
this.diskCacheSize = diskCacheSize;
}
/**
* This method gives the timeout period in milliseconds.
*
* @return timeout in milliseconds
*/
public long getTimeout() {
return timeout / 1000;
}
/**
* This method sets the timeout period as milliseconds.
*
* @param timeout millisecond timeout period to be set.
*/
public void setTimeout(long timeout) {
this.timeout = timeout * 1000;
}
/**
* This method gives SequenceMediator to be executed.
*
* @return sequence mediator to be executed.
*/
public SequenceMediator getOnCacheHitSequence() {
return onCacheHitSequence;
}
/**
* This method sets SequenceMediator to be executed.
*
* @param onCacheHitSequence sequence mediator to be set.
*/
public void setOnCacheHitSequence(SequenceMediator onCacheHitSequence) {
this.onCacheHitSequence = onCacheHitSequence;
}
/**
* This method gives reference to the onCacheHit sequence to be executed.
*
* @return reference to the onCacheHit sequence.
*/
public String getOnCacheHitRef() {
return onCacheHitRef;
}
/**
* This method sets reference to the onCacheHit sequence to be executed.
*
* @param onCacheHitRef reference to the onCacheHit sequence to be set.
*/
public void setOnCacheHitRef(String onCacheHitRef) {
this.onCacheHitRef = onCacheHitRef;
}
/**
* This method gives the maximum size of the messages to be cached in bytes.
*
* @return maximum size of the messages to be cached in bytes.
*/
public int getMaxMessageSize() {
return maxMessageSize;
}
/**
* This method sets the maximum size of the messages to be cached in bytes.
*
* @param maxMessageSize maximum size of the messages to be set in bytes.
*/
public void setMaxMessageSize(int maxMessageSize) {
this.maxMessageSize = maxMessageSize;
}
@Override
public Mediator getInlineSequence(SynapseConfiguration synCfg, int inlinedSeqIdentifier) {
if (inlinedSeqIdentifier == 0) {
if (onCacheHitSequence != null) {
return onCacheHitSequence;
} else if (onCacheHitRef != null) {
return synCfg.getSequence(onCacheHitRef);
}
}
return null;
}
}