/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.mx.metadata;

import java.beans.IntrospectionException;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.management.Descriptor;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.NotCompliantMBeanException;
import javax.management.modelmbean.DescriptorSupport;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;

import org.dom4j.Attribute;
import org.dom4j.Element;
import org.jboss.logging.Logger;
import org.jboss.mx.modelmbean.XMBeanConstants;
import org.jboss.mx.util.JBossNotCompliantMBeanException;
import org.jboss.util.Classes;
import org.jboss.util.StringPropertyReplacer;
import org.jboss.util.propertyeditor.PropertyEditors;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/** 
 * The JBoss 1.0 model mbean descriptor parser class.
 * 
 * @author Matt Munz
 * @author Scott.Stark@jboss.org
 * @author Dimitris.Andreadis@jboss.org
 * @version $Revision$
 */
public class JBossXMBean10 extends AbstractBuilder
   implements XMBeanConstants
{
   private static Logger log = Logger.getLogger(JBossXMBean10.class);

   // Attributes ----------------------------------------------------
   
   private Element element;

   /**
    * The class name of the Model MBean implementation class.
    */
   private String mmbClassName      = null;
   
   /**
    * The class name of the resource object represented by this Model MBean.
    */
   private String resourceClassName = null; 

  
   // Constructors --------------------------------------------------

   public JBossXMBean10(String mmbClassName, String resourceClassName, Element element, Map properties) 
   {
      super();
      this.mmbClassName = mmbClassName;
      this.resourceClassName = resourceClassName;
      this.element = element;
      setProperties(properties);
   }

   // MetaDataBuilder implementation --------------------------------

   public MBeanInfo build() throws NotCompliantMBeanException
   {
      try
      {
         if (element == null)
         {
            throw new JBossNotCompliantMBeanException("No xml configuration supplied!");
         }
         String description = element.elementTextTrim("description");

         if (resourceClassName == null) 
         {
            resourceClassName = element.elementTextTrim("class");
         }
         
         List constructors = element.elements("constructor");
         List operations = element.elements("operation");
         List attributes = element.elements("attribute");
         List notifications = element.elements("notification");

         Descriptor descr = getDescriptor(element, mmbClassName, MBEAN_DESCRIPTOR);

         ModelMBeanInfo info = buildMBeanMetaData(
            description, constructors, operations,
            attributes, notifications, descr
         );

         return (MBeanInfo) info;
      }
      catch (Throwable t)
      {
         throw new JBossNotCompliantMBeanException("Error parsing the XML file: ", t);
      }
   }

   
   // Protected -----------------------------------------------------
   
   protected Descriptor getDescriptor(final Element parent, final String infoName, final String type)
           throws NotCompliantMBeanException
   {
      Descriptor descr = new DescriptorSupport();
      descr.setField(NAME, infoName);
      descr.setField(DISPLAY_NAME, infoName);
      descr.setField(DESCRIPTOR_TYPE, type);

      Element descriptors = parent.element("descriptors");
      if (descriptors == null) 
      {
         return descr;
      }

      for (Iterator i = descriptors.elementIterator(); i.hasNext();)
      {
         Element descriptor = (Element)i.next();
         String name = descriptor.getName();
         if (name.equals("persistence")) 
         {
            String persistPolicy = descriptor.attributeValue(PERSIST_POLICY);
            String persistPeriod = descriptor.attributeValue(PERSIST_PERIOD);
            String persistLocation = descriptor.attributeValue(PERSIST_LOCATION);
            String persistName = descriptor.attributeValue(PERSIST_NAME);
            if (persistPolicy != null)
            {
               validate(persistPolicy, PERSIST_POLICIES);
               descr.setField(PERSIST_POLICY, persistPolicy);
            }
            if (persistPeriod != null)
            {
               descr.setField(PERSIST_PERIOD, persistPeriod);
            }
            if (persistLocation != null)
            {
               descr.setField(PERSIST_LOCATION, persistLocation);
            }
            if (persistName != null)
            {
               descr.setField(PERSIST_NAME, persistName);
            }
         }
         else if (name.equals(CURRENCY_TIME_LIMIT))
         {
            descr.setField(CURRENCY_TIME_LIMIT, descriptor.attributeValue("value"));
         }
         else if (name.equals(DEFAULT))
         {
            String value = descriptor.attributeValue("value");
            descr.setField(DEFAULT, value);
         }
         else if (name.equals("display-name"))//DISPLAY_NAME is displayname
         {
            String value = descriptor.attributeValue("value");
            descr.setField(DISPLAY_NAME, value);
         }
         else if (name.equals(CACHED_VALUE))
         {
            String value = descriptor.attributeValue("value");
            descr.setField(CACHED_VALUE, value);
         }
         else if (name.equals(PERSISTENCE_MANAGER))
         {
            descr.setField(PERSISTENCE_MANAGER, descriptor.attributeValue("value"));
         }
         else if (name.equals(DESCRIPTOR))
         {
            descr.setField(descriptor.attributeValue("name"), descriptor.attributeValue("value"));
         }
         else if (name.equals("injection"))
         {
            descr.setField(descriptor.attributeValue("id"), descriptor.attributeValue("setMethod"));
         }
         else if(name.equals(INTERCEPTORS))
         {
            Descriptor[] interceptorDescriptors = buildInterceptors(descriptor);
            descr.setField(INTERCEPTORS, interceptorDescriptors);
         }         
      } // end of for ()

      return descr;
   }

   private void validate(String value, String[] valid) throws NotCompliantMBeanException
   {
      for (int i = 0; i< valid.length; i++)
      {
         if (valid[i].equalsIgnoreCase(value)) 
         {
            return;
         } // end of if ()
      } // end of for ()
      throw new JBossNotCompliantMBeanException("Unknown descriptor value: " + value);      
   }


   // builder methods

   protected ModelMBeanInfo buildMBeanMetaData(String description,
                                               List constructors, List operations, List attributes,
                                               List notifications, Descriptor descr)
      throws NotCompliantMBeanException
   {

      ModelMBeanOperationInfo[] operInfo =
         buildOperationInfo(operations);
      ModelMBeanAttributeInfo[] attrInfo =
         buildAttributeInfo(attributes);
      ModelMBeanConstructorInfo[] constrInfo =
         buildConstructorInfo(constructors);
      ModelMBeanNotificationInfo[] notifInfo =
         buildNotificationInfo(notifications);

      ModelMBeanInfo info = new ModelMBeanInfoSupport(
         mmbClassName, description, attrInfo, constrInfo,
         operInfo, notifInfo, descr
      );

      return info;
   }


   protected ModelMBeanConstructorInfo[] buildConstructorInfo(List constructors)
      throws NotCompliantMBeanException
   {

      List infos = new ArrayList();

      for (Iterator it = constructors.iterator(); it.hasNext();)
      {
         Element constr = (Element) it.next();
         String name = constr.elementTextTrim("name");
         String description = constr.elementTextTrim("description");
         List params = constr.elements("parameter");

         MBeanParameterInfo[] paramInfo =
            buildParameterInfo(params);

         Descriptor descr = getDescriptor(constr, name, OPERATION_DESCRIPTOR);
         descr.setField(ROLE,  ROLE_CONSTRUCTOR);

         ModelMBeanConstructorInfo info =
            new ModelMBeanConstructorInfo(name, description, paramInfo, descr);

         infos.add(info);
      }

      return (ModelMBeanConstructorInfo[]) infos.toArray(
         new ModelMBeanConstructorInfo[0]);
   }

   protected ModelMBeanOperationInfo[] buildOperationInfo(List operations)
      throws NotCompliantMBeanException
   {
      List infos = new ArrayList();

      for (Iterator it = operations.iterator(); it.hasNext(); )
      {
         Element oper = (Element) it.next();
         String name = oper.elementTextTrim("name");
         String description = oper.elementTextTrim("description");
         String type = oper.elementTextTrim("return-type");
         String impact = oper.attributeValue("impact");
         List params = oper.elements("parameter");

         MBeanParameterInfo[] paramInfo =
            buildParameterInfo(params);

         Descriptor descr = getDescriptor(oper, name, OPERATION_DESCRIPTOR);
         descr.setField(ROLE, ROLE_OPERATION);

         // defaults to ACTION_INFO
         int operImpact = MBeanOperationInfo.ACTION_INFO;

         if (impact != null)
         {
            if (impact.equals(INFO))
               operImpact = MBeanOperationInfo.INFO;
            else if (impact.equals(ACTION))
               operImpact = MBeanOperationInfo.ACTION;
            else if (impact.equals(ACTION_INFO))
               operImpact = MBeanOperationInfo.ACTION_INFO;
         }

         // default return-type is void
         if (type == null)
            type = "void";

         ModelMBeanOperationInfo info = new ModelMBeanOperationInfo(
            name, description, paramInfo, type, operImpact, descr);

         infos.add(info);
      }

      return (ModelMBeanOperationInfo[]) infos.toArray(
         new ModelMBeanOperationInfo[0]);
   }


   protected ModelMBeanNotificationInfo[] buildNotificationInfo(List notifications)
      throws NotCompliantMBeanException
   {

      List infos = new ArrayList();

      for (Iterator it = notifications.iterator(); it.hasNext();)
      {
         Element notif = (Element) it.next();
         String name = notif.elementTextTrim("name");
         String description = notif.elementTextTrim("description");
         List notifTypes = notif.elements("notification-type");
         Descriptor descr = getDescriptor(notif, name, NOTIFICATION_DESCRIPTOR);

         List types = new ArrayList();

         for (Iterator iterator = notifTypes.iterator(); iterator.hasNext();)
         {
            Element type = (Element) iterator.next();
            types.add(type.getTextTrim());
         }

         ModelMBeanNotificationInfo info = new ModelMBeanNotificationInfo(
            (String[]) types.toArray(new String[types.size()]), name, description, descr);

         infos.add(info);
      }

      return (ModelMBeanNotificationInfo[]) infos.toArray(
         new ModelMBeanNotificationInfo[infos.size()]
      );
   }

   protected ModelMBeanAttributeInfo[] buildAttributeInfo(List attributes)
      throws NotCompliantMBeanException
   {

      List infos = new ArrayList();

      for (Iterator it = attributes.iterator(); it.hasNext();)
      {
         Element attr = (Element) it.next();
         String name = attr.elementTextTrim("name");
         String description = attr.elementTextTrim("description");
         String type = attr.elementTextTrim("type");
         String access = attr.attributeValue("access");
         String getMethod = attr.attributeValue("getMethod");
         String setMethod = attr.attributeValue("setMethod");
         Descriptor descr = getDescriptor(attr, name, ATTRIBUTE_DESCRIPTOR);
         //Convert types here from string to specified type
         String unconvertedValue = (String)descr.getFieldValue(CACHED_VALUE);
         if (unconvertedValue != null && !"java.lang.String".equals(type))
         {
            descr.setField(CACHED_VALUE, convertValue(unconvertedValue, type));
         }
         else
         {
            // if <value value="xxx"/> is absent
            // try new syntax for VALUE initialization
            // e.g <value><nested-element/></value>
            Object value = getAttributeValue(attr, type, CACHED_VALUE);
            if (value != null)
               descr.setField(CACHED_VALUE, value);
         }         
         String unconvertedDefault = (String)descr.getFieldValue(DEFAULT);
         if (unconvertedDefault != null && !"java.lang.String".equals(type))
         {
            descr.setField(DEFAULT, convertValue(unconvertedDefault, type));
         }
         else
         {
            // if <defaul value="xxx"/> is absent
            // try new syntax for DEFAULT initialization
            // e.g <default><nested-element/></default>
            Object value = getAttributeValue(attr, type, DEFAULT);
            if (value != null)
               descr.setField(DEFAULT, value);
         }             
         if (getMethod != null) 
         {
            descr.setField(GET_METHOD, getMethod);
         } // end of if ()
         
         if (setMethod != null) 
         {
            descr.setField(SET_METHOD, setMethod);
         } // end of if ()
         

         // defaults read-write
         boolean isReadable = true;
         boolean isWritable = true;

         if (access.equalsIgnoreCase("read-only"))
            isWritable = false;

         else if (access.equalsIgnoreCase("write-only"))
            isReadable = false;


         ModelMBeanAttributeInfo info = new ModelMBeanAttributeInfo(
            name, type, description, isReadable, isWritable, false, descr
         );


         infos.add(info);
      }

      return (ModelMBeanAttributeInfo[]) infos.toArray(
         new ModelMBeanAttributeInfo[0]
      );
   }

   /**
    * Get the value for the attribute descriptor "value" or "default"
    * the same way we would do for mbean attribute overrides
    */
   protected Object getAttributeValue(Element attribute, String typeName, String which)
      throws NotCompliantMBeanException
   {
      Object value = null;
      
      Element descriptors = attribute.element("descriptors");
      if (descriptors != null) 
      {
         for (Iterator i = descriptors.elementIterator(); i.hasNext();)
         {
            // looking for 'which', i.e. "value" or "default"
            Element descriptor = (Element)i.next();
            String name = descriptor.getName();
            if (name.equals(which) && descriptor.hasContent()) 
            {
               // at this point "value" attribute does not exist
               // plus the descriptor has content so we know the
               // new syntax is used.
               // 
               // Convert to org.w3c.dom.Element so that the code
               // from ServiceConfigurator can be applied, plus
               // if attribute type is org.w3c.dom.Element we need
               // to make the conversion anyway.
               
               // descriptor(org.dom4j.Element) -> element (org.w3c.dom.Element)
               try
               {
                  org.w3c.dom.Element element = toW3CElement(descriptor);
                  
                  boolean replace = true;
                  boolean trim = true;
                  
                  String replaceAttr = element.getAttribute("replace");
                  if( replaceAttr.length() > 0 )
                     replace = Boolean.valueOf(replaceAttr).booleanValue();
                  String trimAttr = element.getAttribute("trim");
                  if( trimAttr.length() > 0 )
                     trim = Boolean.valueOf(trimAttr).booleanValue();
                  
                  // Get the classloader for loading attribute classes.
                  ClassLoader cl = Thread.currentThread().getContextClassLoader();
                  
                  // see if it is a primitive type first
                  Class typeClass = Classes.getPrimitiveTypeForName(typeName);
                  if (typeClass == null)
                  {
                     // nope try look up
                     try
                     {
                        typeClass = cl.loadClass(typeName);
                     }
                     catch (ClassNotFoundException e)
                     {
                        throw new JBossNotCompliantMBeanException
                           ("Class not found '" + typeName + "'", e);
                     }
                  }

                  /* Attributes of type Element are passed as is after optionally
                  performing system property replacement
                  */
                  if (typeClass.equals(org.w3c.dom.Element.class))
                  {
                     // Use the first child Element of this element as the value
                     NodeList nl = element.getChildNodes();
                     for (int j=0; j < nl.getLength(); j++)
                     {
                        Node n = nl.item(j);
                        if (n.getNodeType() == Node.ELEMENT_NODE)
                        {
                           value = n;
                           break;
                        }
                     }
                     // Replace any ${x} references in the element text
                     if( replace )
                     {
                        PropertyEditor editor = PropertyEditorManager.findEditor(typeClass);
                        if( editor == null )
                        {
                           log.warn("Cannot perform property replace on Element");
                        }
                        else
                        {
                           editor.setValue(value);
                           String text = editor.getAsText();
                           text = StringPropertyReplacer.replaceProperties(text);
                           editor.setAsText(text);
                           value = editor.getValue();
                        }
                     }
                  }

                  if (value == null)
                  {
                     PropertyEditor editor = PropertyEditorManager.findEditor(typeClass);
                     if (editor == null)
                     {
                        throw new JBossNotCompliantMBeanException
                           ("No property editor for type '" + typeName + "'");
                     }
                     // Get the attribute value
                     String attributeText = getElementContent(element, trim, replace);
                     editor.setAsText(attributeText);
                     value = editor.getValue();
                  }
               }
               catch (org.dom4j.DocumentException e)
               {
                  throw new JBossNotCompliantMBeanException(
                        "cannot convert '" + which + "' descriptor to org.w3c.dom.Element", e);
               }

               // stop processing
               break;
            }
         }
      }
      return value;
   }

   /**
    * Convert org.dom4j.Element->org.w3c.dom.Element
    */
   private org.w3c.dom.Element toW3CElement(org.dom4j.Element d4element)
      throws org.dom4j.DocumentException
   {
      // prepare
      org.dom4j.Document d4doc = org.dom4j.DocumentFactory.getInstance().createDocument();
      org.dom4j.io.DOMWriter d4Writer = new org.dom4j.io.DOMWriter();
      // copy
      d4doc.setRootElement(d4element.createCopy());
      // convert
      org.w3c.dom.Document doc = d4Writer.write(d4doc);
      // return root Element - should I copy again?
      return doc.getDocumentElement();
   }
   
   /**
    * Copied from ServiceConfigurator
    */
   private String getElementContent(org.w3c.dom.Element element, boolean trim, boolean replace)
   {
      NodeList nl = element.getChildNodes();
      String attributeText = "";
      for (int i = 0; i < nl.getLength(); i++)
      {
         Node n = nl.item(i);
         if( n instanceof org.w3c.dom.Text )
         {
            attributeText += ((org.w3c.dom.Text)n).getData();
         }
      } // end of for ()
      if( trim )
         attributeText = attributeText.trim();
      if (replace)
         attributeText = StringPropertyReplacer.replaceProperties(attributeText);
      return attributeText;
   }
   
   /**
    * Describe <code>convertType</code> method here.
    * Copied from ServiceConfigurator, without Element support.
    *
    * @param unconverted a <code>String</code> value
    * @param typeName a <code>String</code> value
    * @return an <code>Object</code> value
    * @exception NotCompliantMBeanException if an error occurs
    */
   protected Object convertValue(String unconverted, String typeName)
      throws NotCompliantMBeanException
   {
      Object value = null;
      try
      {
         value = PropertyEditors.convertValue(unconverted, typeName);
      }
      catch (ClassNotFoundException e)
      {
         log.debug("Failed to load type class", e);
         throw new NotCompliantMBeanException
               ("Class not found for type: " + typeName);
      }
      catch(IntrospectionException e)
      {
         throw new NotCompliantMBeanException
               ("No property editor for type=" + typeName);
      }
      return value;
   }

   protected MBeanParameterInfo[] buildParameterInfo(List parameters)
   {
      Iterator it = parameters.iterator();
      List infos = new ArrayList();

      while (it.hasNext())
      {
         Element param = (Element) it.next();
         String name = param.elementTextTrim("name");
         String type = param.elementTextTrim("type");
         String descr = param.elementTextTrim("description");

         MBeanParameterInfo info = new MBeanParameterInfo(name, type, descr);

         infos.add(info);
      }
      
      return (MBeanParameterInfo[]) infos.toArray(new MBeanParameterInfo[0]);
   }

   protected Descriptor[] buildInterceptors(Element descriptor)
   {
      List interceptors = descriptor.elements("interceptor");
      ArrayList tmp = new ArrayList();
      for(int i = 0; i < interceptors.size(); i ++)
      {
         Element interceptor = (Element) interceptors.get(i);
         String code = interceptor.attributeValue("code");
         DescriptorSupport interceptorDescr = new DescriptorSupport();
         interceptorDescr.setField("code", code);
         List attributes = interceptor.attributes();
         for(int a = 0; a < attributes.size(); a ++)
         {
            Attribute attr = (Attribute) attributes.get(a);
            String name = attr.getName();
            String value = attr.getValue();
            value = StringPropertyReplacer.replaceProperties(value);
            interceptorDescr.setField(name, value);
         }
         tmp.add(interceptorDescr);
      }
      Descriptor[] descriptors = new Descriptor[tmp.size()];
      tmp.toArray(descriptors);
      return descriptors;
   }

}
