/*
 * 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 final 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;
      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
            if (log.isDebugEnabled())
               log.debug("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)
      {
         if (strBuddyCommunicationTimeout != null && strBuddyCommunicationTimeout.trim().length() != 0)
         {
            throw new ConfigurationException("Bad buddyCommunicationTimeout [" + strBuddyCommunicationTimeout + "]");
         }
      }
      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;
         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 missingPolicy)
               {
                  LogFactory.getLog(EvictionConfig.class).warn(missingPolicy.getLocalizedMessage());
                  throw missingPolicy;
               }
            }

            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;
      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;
      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)
   {
      StringBuilder buffer = new StringBuilder();
      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
   {
      final Map<String, String> stringAttribs;
      final Map<String, Element> xmlAttribs;

      ParsedAttributes(Map strings, Map elements)
      {
         this.stringAttribs = strings;
         this.xmlAttribs = elements;
      }
   }
}
