/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.cache.factories;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.buddyreplication.NextMemberBuddyLocator;
import org.jboss.cache.config.BuddyReplicationConfig;
import org.jboss.cache.config.BuddyReplicationConfig.BuddyLocatorConfig;
import org.jboss.cache.config.CacheLoaderConfig;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig.SingletonStoreConfig;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.ConfigurationException;
import org.jboss.cache.config.EvictionConfig;
import org.jboss.cache.config.EvictionPolicyConfig;
import org.jboss.cache.config.EvictionRegionConfig;
import org.jboss.cache.config.MissingPolicyException;
import org.jboss.cache.eviction.EvictionPolicy;
import org.jboss.cache.util.BeanUtils;
import org.jboss.cache.util.Util;
import org.jboss.cache.xml.XmlHelper;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
/**
* Reads in XMLconfiguration files and spits out a {@link org.jboss.cache.config.Configuration} object. When deployed as a
* JBoss MBean, this role is performed by the JBoss Microcontainer. This class is only used internally in unit tests
* or within {@link org.jboss.cache.CacheFactory} implementations for standalone JBoss Cache usage.
*
* @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
* @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
* @since 2.00.
*/
public class XmlConfigurationParser
{
private static Log log = LogFactory.getLog(XmlConfigurationParser.class);
public static final String ATTR = "attribute";
public static final String NAME = "name";
/**
* Parses an XML file and returns a new configuration. This method attempts to look for the file name passed in on
* the classpath. If not found, it will search for the file on the file system instead, treating the name as an
* absolute path.
*
* @param filename the name of the XML file to parse.
* @return a configured Configuration object representing the configuration in the file
*/
public Configuration parseFile(String filename)
{
InputStream is = getAsInputStreamFromClassLoader(filename);
if (is == null)
{
if (log.isDebugEnabled())
log.debug("Unable to find configuration file " + filename + " in classpath; searching for this file on the filesystem instead.");
try
{
is = new FileInputStream(filename);
}
catch (FileNotFoundException e)
{
throw new ConfigurationException("Unable to find config file " + filename + " either in classpath or on the filesystem!", e);
}
}
return parseStream(is);
}
/**
* Parses an input stream containing XML text and returns a new configuration.
*
* @param stream input stream to parse. SHould not be null.
* @return a configured Configuration object representing the configuration in the stream
* @since 2.1.0
*/
public Configuration parseStream(InputStream stream)
{
// loop through all elements in XML.
Element root = XmlHelper.getDocumentRoot(stream);
Element mbeanElement = getMBeanElement(root);
return parseConfiguration(mbeanElement);
}
public Configuration parseConfiguration(Element configurationRoot)
{
ParsedAttributes attributes = extractAttributes(configurationRoot);
// Deal with legacy attributes we no longer support
handleRemovedAttributes(attributes);
// Deal with legacy attributes that we renamed or otherwise altered
handleRenamedAttributes(attributes);
Configuration c = new Configuration();
setValues(c, attributes.stringAttribs, false);
// Special handling for XML elements -- we hard code the parsing
setXmlValues(c, attributes.xmlAttribs);
return c;
}
/**
* Check for and remove any attributes that were supported in the
* 1.x releases and no longer are. Log a WARN or throw a
* {@link ConfigurationException} if any are found. Which is done depends
* on the attribute:
* <p/>
* <ul>
* <li><i>MultiplexerService</i> -- throws an Exception</li>
* <li><i>ServiceName</i> -- logs a WARN</li>
* </ul>
*
* @param attributes
*/
protected void handleRemovedAttributes(ParsedAttributes attributes)
{
String evictionPolicy = attributes.stringAttribs.remove("EvictionPolicyClass");
if (evictionPolicy != null)
{
throw new ConfigurationException("XmlConfigurationParser does not " +
"support the JBC 1.x attribute EvictionPolicyClass. Set the default " +
"eviction policy via the policyClass element in the EvictionConfig section");
}
String multiplexerService = attributes.stringAttribs.remove("MultiplexerService");
if (multiplexerService != null)
{
throw new ConfigurationException("XmlConfigurationParser does not " +
"support the JBC 1.x attribute MultiplexerService. Inject the " +
"multiplexer directly using Configuration.getRuntimeConfig().setMuxChannelFactory()");
}
String serviceName = attributes.stringAttribs.remove("ServiceName");
if (serviceName != null)
{
log.warn("XmlConfigurationParser does not support the deprecated " +
"attribute ServiceName. If JMX registration is needed, " +
"register a CacheJmxWrapper or PojoCacheJmxWrapper in " +
"JMX with the desired name");
}
}
/**
* Check for any attributes that were supported in the
* 1.x releases but whose name has changed. Log a WARN if any are found, but
* convert the attribute to the new name.
* <p/>
* <ul>
* <li><i>UseMbean</i> becomes <i>ExposeManagementStatistics</i></li>
* </ul>
*
* @param attributes
*/
private void handleRenamedAttributes(ParsedAttributes attributes)
{
String keepStats = attributes.stringAttribs.remove("UseInterceptorMbeans");
if (keepStats != null && attributes.stringAttribs.get("ExposeManagementStatistics") == null)
{
log.warn("Found non-existent JBC 1.x attribute 'UseInterceptorMbeans' and replaced " +
"with 'ExposeManagementStatistics'. Please update your config " +
"to use the new attribute name");
attributes.stringAttribs.put("ExposeManagementStatistics", keepStats);
}
Element clc = attributes.xmlAttribs.remove("CacheLoaderConfiguration");
if (clc != null && attributes.xmlAttribs.get("CacheLoaderConfig") == null)
{
log.warn("Found non-existent JBC 1.x attribute 'CacheLoaderConfiguration' and replaced " +
"with 'CacheLoaderConfig'. Please update your config " +
"to use the new attribute name");
attributes.xmlAttribs.put("CacheLoaderConfig", clc);
}
}
protected InputStream getAsInputStreamFromClassLoader(String filename)
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
InputStream is = cl == null ? null : cl.getResourceAsStream(filename);
if (is == null)
{
// check system class loader
is = getClass().getClassLoader().getResourceAsStream(filename);
}
return is;
}
protected Element getMBeanElement(Element root)
{
// This is following JBoss convention.
NodeList list = root.getElementsByTagName(XmlHelper.ROOT);
if (list == null) throw new ConfigurationException("Can't find " + XmlHelper.ROOT + " tag");
if (list.getLength() > 1) throw new ConfigurationException("Has multiple " + XmlHelper.ROOT + " tag");
Node node = list.item(0);
Element element = null;
if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE)
{
element = (Element) node;
}
else
{
throw new ConfigurationException("Can't find " + XmlHelper.ROOT + " element");
}
return element;
}
protected static void setValues(Object target, Map<?, ?> attribs, boolean isXmlAttribs)
{
Class objectClass = target.getClass();
// go thru simple string setters first.
for (Entry entry : attribs.entrySet())
{
String propName = (String) entry.getKey();
String setter = BeanUtils.setterName(propName);
Method method;
try
{
if (isXmlAttribs)
{
method = objectClass.getMethod(setter, Element.class);
method.invoke(target, entry.getValue());
}
else
{
method = objectClass.getMethod(setter, String.class);
method.invoke(target, entry.getValue());
}
continue;
}
catch (NoSuchMethodException me)
{
// this is ok, but certainly log this as a warning
// this is hugely noisy!
//if (log.isWarnEnabled()) log.warn("Unrecognised attribute " + propName + ". Please check your configuration. Ignoring!!");
}
catch (Exception e)
{
throw new ConfigurationException("Unable to invoke setter " + setter + " on " + objectClass, e);
}
// if we get here, we could not find a String or Element setter.
for (Method m : objectClass.getMethods())
{
if (setter.equals(m.getName()))
{
Class paramTypes[] = m.getParameterTypes();
if (paramTypes.length != 1)
{
throw new ConfigurationException("Setter " + setter + " does not contain the expected number of params. Has " + paramTypes.length + " instead of just 1.");
}
Class parameterType = paramTypes[0];
PropertyEditor editor = PropertyEditorManager.findEditor(parameterType);
if (editor == null)
{
throw new ConfigurationException("Couldn't find a property editor for parameter type " + parameterType);
}
editor.setAsText((String) attribs.get(propName));
Object parameter = editor.getValue();
//if (log.isDebugEnabled()) log.debug("Invoking setter method: " + setter + " with parameter \"" + parameter + "\" of type " + parameter.getClass());
try
{
m.invoke(target, parameter);
}
catch (Exception e)
{
throw new ConfigurationException("Unable to invoke setter " + setter + " on " + objectClass, e);
}
}
}
}
}
protected void setXmlValues(Configuration conf, Map<String, Element> attribs)
{
for (Entry<String, Element> entry : attribs.entrySet())
{
String propname = entry.getKey();
if ("BuddyReplicationConfiguration".equals(propname)
|| "BuddyReplicationConfig".equals(propname))
{
BuddyReplicationConfig brc = parseBuddyReplicationConfig(entry.getValue());
conf.setBuddyReplicationConfig(brc);
}
else if ("CacheLoaderConfiguration".equals(propname)
|| "CacheLoaderConfig".equals(propname))
{
CacheLoaderConfig clc = parseCacheLoaderConfig(entry.getValue());
conf.setCacheLoaderConfig(clc);
}
else if ("EvictionPolicyConfiguration".equals(propname)
|| "EvictionPolicyConfig".equals(propname))
{
EvictionConfig ec = parseEvictionConfig(entry.getValue());
conf.setEvictionConfig(ec);
}
else if ("ClusterConfig".equals(propname))
{
String jgc = parseClusterConfigXml(entry.getValue());
conf.setClusterConfig(jgc);
}
else
{
throw new ConfigurationException("Unknown configuration element " + propname);
}
}
}
public static BuddyReplicationConfig parseBuddyReplicationConfig(Element element)
{
BuddyReplicationConfig brc = new BuddyReplicationConfig();
brc.setEnabled(XmlHelper.readBooleanContents(element, "buddyReplicationEnabled"));
brc.setDataGravitationRemoveOnFind(XmlHelper.readBooleanContents(element, "dataGravitationRemoveOnFind", true));
brc.setDataGravitationSearchBackupTrees(XmlHelper.readBooleanContents(element, "dataGravitationSearchBackupTrees", true));
brc.setAutoDataGravitation(brc.isEnabled() && XmlHelper.readBooleanContents(element, "autoDataGravitation", false));
String strBuddyCommunicationTimeout = XmlHelper.readStringContents(element, "buddyCommunicationTimeout");
try
{
brc.setBuddyCommunicationTimeout(Integer.parseInt(strBuddyCommunicationTimeout));
}
catch (Exception e)
{
}
finally
{
if (log.isDebugEnabled())
{
log.debug("Using buddy communication timeout of " + brc.getBuddyCommunicationTimeout() + " millis");
}
}
String buddyPoolName = XmlHelper.readStringContents(element, "buddyPoolName");
if ("".equals(buddyPoolName))
{
buddyPoolName = null;
}
brc.setBuddyPoolName(buddyPoolName);
// now read the buddy locator details
String buddyLocatorClass = XmlHelper.readStringContents(element, "buddyLocatorClass");
if (buddyLocatorClass == null || buddyLocatorClass.length() == 0)
{
buddyLocatorClass = NextMemberBuddyLocator.class.getName();
}
Properties props = null;
try
{
props = XmlHelper.readPropertiesContents(element, "buddyLocatorProperties");
}
catch (IOException e)
{
log.warn("Caught exception reading buddyLocatorProperties", e);
log.error("Unable to read buddyLocatorProperties specified! Using defaults for [" + buddyLocatorClass + "]");
}
BuddyLocatorConfig blc = new BuddyLocatorConfig();
blc.setBuddyLocatorClass(buddyLocatorClass);
blc.setBuddyLocatorProperties(props);
brc.setBuddyLocatorConfig(blc);
return brc;
}
public static CacheLoaderConfig parseCacheLoaderConfig(Element element)
{
CacheLoaderConfig clc = new CacheLoaderConfig();
clc.setPassivation(XmlHelper.readBooleanContents(element, "passivation"));
clc.setPreload(XmlHelper.readStringContents(element, "preload"));
clc.setShared(XmlHelper.readBooleanContents(element, "shared"));
NodeList cacheLoaderNodes = element.getElementsByTagName("cacheloader");
for (int i = 0; i < cacheLoaderNodes.getLength(); i++)
{
Node node = cacheLoaderNodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE)
{
Element indivElement = (Element) node;
CacheLoaderConfig.IndividualCacheLoaderConfig iclc = new CacheLoaderConfig.IndividualCacheLoaderConfig();
iclc.setAsync(XmlHelper.readBooleanContents(indivElement, "async", false));
iclc.setIgnoreModifications(XmlHelper.readBooleanContents(indivElement, "ignoreModifications", false));
iclc.setFetchPersistentState(XmlHelper.readBooleanContents(indivElement, "fetchPersistentState", false));
iclc.setPurgeOnStartup(XmlHelper.readBooleanContents(indivElement, "purgeOnStartup", false));
iclc.setClassName(XmlHelper.readStringContents(indivElement, "class"));
try
{
iclc.setProperties(XmlHelper.readPropertiesContents(indivElement, "properties"));
}
catch (IOException e)
{
throw new ConfigurationException("Problem loader cache loader properties", e);
}
SingletonStoreConfig ssc = parseSingletonStoreConfig(indivElement);
if (ssc != null)
{
iclc.setSingletonStoreConfig(ssc);
}
clc.addIndividualCacheLoaderConfig(iclc);
}
}
return clc;
}
private static SingletonStoreConfig parseSingletonStoreConfig(Element cacheLoaderelement)
{
/* singletonStore element can only appear once in a cacheloader, so we just take the first one ignoring any
subsequent definitions in cacheloader element*/
Node singletonStoreNode = cacheLoaderelement.getElementsByTagName("singletonStore").item(0);
if (singletonStoreNode != null && singletonStoreNode.getNodeType() == Node.ELEMENT_NODE)
{
Element singletonStoreElement = (Element) singletonStoreNode;
boolean singletonStoreEnabled = XmlHelper.readBooleanContents(singletonStoreElement, "enabled");
String singletonStoreClass = XmlHelper.readStringContents(singletonStoreElement, "class");
Properties singletonStoreproperties = null;
try
{
singletonStoreproperties = XmlHelper.readPropertiesContents(singletonStoreElement, "properties");
}
catch (IOException e)
{
throw new ConfigurationException("Problem loading singleton store properties", e);
}
SingletonStoreConfig ssc = new SingletonStoreConfig();
ssc.setSingletonStoreEnabled(singletonStoreEnabled);
ssc.setSingletonStoreClass(singletonStoreClass);
ssc.setSingletonStoreproperties(singletonStoreproperties);
return ssc;
}
return null;
}
public static EvictionConfig parseEvictionConfig(Element element)
{
EvictionConfig ec = new EvictionConfig();
if (element != null)
{
// If they set the default eviction policy in the element, use that
// in preference to the external attribute
String temp = XmlHelper.getTagContents(element,
EvictionConfig.EVICTION_POLICY_CLASS, ATTR, NAME);
if (temp != null && temp.length() > 0)
{
ec.setDefaultEvictionPolicyClass(temp);
}
temp = XmlHelper.getTagContents(element,
EvictionConfig.WAKEUP_INTERVAL_SECONDS, ATTR, NAME);
int wakeupIntervalSeconds = 0;
if (temp != null)
{
wakeupIntervalSeconds = Integer.parseInt(temp);
}
if (wakeupIntervalSeconds <= 0)
{
wakeupIntervalSeconds = EvictionConfig.WAKEUP_DEFAULT;
}
ec.setWakeupIntervalSeconds(wakeupIntervalSeconds);
int eventQueueSize = 0;
temp = XmlHelper.getTagContents(element,
EvictionConfig.EVENT_QUEUE_SIZE, ATTR, NAME);
if (temp != null)
{
eventQueueSize = Integer.parseInt(temp);
}
if (eventQueueSize <= 0)
{
eventQueueSize = EvictionConfig.EVENT_QUEUE_SIZE_DEFAULT;
}
ec.setDefaultEventQueueSize(eventQueueSize);
NodeList list = element.getElementsByTagName(EvictionRegionConfig.REGION);
if (list != null && list.getLength() > 0)
{
List regionConfigs = new ArrayList(list.getLength());
for (int i = 0; i < list.getLength(); i++)
{
org.w3c.dom.Node node = list.item(i);
if (node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE)
{
continue;
}
try
{
regionConfigs.add(parseEvictionRegionConfig((Element) node, ec.getDefaultEvictionPolicyClass(), eventQueueSize));
}
catch (MissingPolicyException ignored)
{
// Just log a warning and continue
// TODO (BES Oct-2006) I did it this way as that is how it worked
// before, but why not just throw the exception?
LogFactory.getLog(EvictionConfig.class).warn(ignored.getLocalizedMessage());
}
}
ec.setEvictionRegionConfigs(regionConfigs);
}
}
return ec;
}
public static EvictionRegionConfig parseEvictionRegionConfig(Element element,
String defaultEvictionClass,
int defaultQueueCapacity)
{
EvictionRegionConfig erc = new EvictionRegionConfig();
erc.setRegionName(element.getAttribute(EvictionRegionConfig.NAME));
String temp = element.getAttribute(EvictionRegionConfig.EVENT_QUEUE_SIZE);
if (temp != null && temp.length() > 0)
{
erc.setEventQueueSize(Integer.parseInt(temp));
}
else
{
erc.setEventQueueSize(defaultQueueCapacity);
}
String evictionClass = element.getAttribute(EvictionRegionConfig.REGION_POLICY_CLASS);
if (evictionClass == null || evictionClass.length() == 0)
{
evictionClass = defaultEvictionClass;
// if it's still null... what do we setCache?
if (evictionClass == null || evictionClass.length() == 0)
{
throw new MissingPolicyException(
"There is no Eviction Policy Class specified on the region or for the entire cache!");
}
}
EvictionPolicy policy = null;
try
{
policy = (EvictionPolicy) Util.loadClass(evictionClass).newInstance();
}
catch (RuntimeException e)
{
throw e;
}
catch (Exception e)
{
throw new RuntimeException("Eviction class is not properly loaded in classloader", e);
}
EvictionPolicyConfig epc = null;
try
{
epc = policy.getEvictionConfigurationClass().newInstance();
}
catch (RuntimeException e)
{
throw e;
}
catch (Exception e)
{
throw new RuntimeException("Failed to instantiate eviction configuration of class " +
policy.getEvictionConfigurationClass(), e);
}
parseEvictionPolicyConfig(element, epc);
erc.setEvictionPolicyConfig(epc);
return erc;
}
public static void parseEvictionPolicyConfig(Element element, EvictionPolicyConfig target)
{
target.reset();
ParsedAttributes attributes = extractAttributes(element);
setValues(target, attributes.stringAttribs, false);
setValues(target, attributes.xmlAttribs, true);
target.validate();
}
/**
* Parses the cluster config which is used to start a JGroups channel
*
* @param config an old-style JGroups protocol config String
*/
public static String parseClusterConfigXml(Element config)
{
StringBuffer buffer = new StringBuffer();
NodeList stack = config.getChildNodes();
int length = stack.getLength();
for (int s = 0; s < length; s++)
{
org.w3c.dom.Node node = stack.item(s);
if (node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE)
{
continue;
}
Element tag = (Element) node;
String protocol = tag.getTagName();
buffer.append(protocol);
NamedNodeMap attrs = tag.getAttributes();
int attrLength = attrs.getLength();
if (attrLength > 0)
{
buffer.append('(');
}
for (int a = 0; a < attrLength; a++)
{
Attr attr = (Attr) attrs.item(a);
String name = attr.getName();
String value = attr.getValue();
buffer.append(name);
buffer.append('=');
buffer.append(value);
if (a < attrLength - 1)
{
buffer.append(';');
}
}
if (attrLength > 0)
{
buffer.append(')');
}
buffer.append(':');
}
// Remove the trailing ':'
buffer.setLength(buffer.length() - 1);
return buffer.toString();
}
protected static ParsedAttributes extractAttributes(Element source)
{
Map<String, String> stringAttribs = new HashMap<String, String>();
Map<String, Element> xmlAttribs = new HashMap<String, Element>();
NodeList list = source.getElementsByTagName(XmlHelper.ATTR);
if (log.isDebugEnabled()) log.debug("Attribute size: " + list.getLength());
// loop through attributes
for (int loop = 0; loop < list.getLength(); loop++)
{
Node node = list.item(loop);
if (node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE) continue;
// for each element (attribute) ...
Element element = (Element) node;
String name = element.getAttribute(XmlHelper.NAME);
String valueStr = XmlHelper.getElementContent(element, true);
Element valueXml = null;
if (valueStr.length() == 0)
{
// This may be an XML element ...
valueXml = XmlHelper.getConfigSubElement(element);
}
// add these to the maps.
if (valueStr.length() > 0) stringAttribs.put(name, valueStr);
if (valueXml != null) xmlAttribs.put(name, valueXml);
}
return new ParsedAttributes(stringAttribs, xmlAttribs);
}
static class ParsedAttributes
{
Map<String, String> stringAttribs;
Map<String, Element> xmlAttribs;
ParsedAttributes(Map strings, Map elements)
{
this.stringAttribs = strings;
this.xmlAttribs = elements;
}
}
}