/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.enterprise.admin.event;

import java.lang.reflect.Constructor;

import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import com.sun.enterprise.admin.event.AdminEvent;
import com.sun.enterprise.config.ConfigContext;
import com.sun.enterprise.config.ConfigChange;
import com.sun.enterprise.config.ConfigAdd;
import com.sun.enterprise.config.ConfigSet;
import com.sun.enterprise.config.ConfigUpdate;
import com.sun.enterprise.config.ConfigDelete;
import com.sun.enterprise.config.serverbeans.ServerTags;
import com.sun.enterprise.admin.pluggable.RestartExtensionFeature;
import com.sun.enterprise.admin.server.core.AdminService;
import com.sun.enterprise.admin.meta.MBeanRegistry;
import com.sun.enterprise.admin.meta.MBeanRegistryFactory;
import com.sun.enterprise.admin.meta.MBeanRegistryEntry;
import com.sun.enterprise.admin.meta.naming.MBeanNamingDescriptor;
import com.sun.enterprise.admin.target.Target;
import com.sun.enterprise.admin.target.TargetBuilder;
import com.sun.enterprise.admin.meta.MBeanMetaException;
import com.sun.enterprise.admin.common.constant.AdminConstants;
import java.util.logging.Level;
import java.util.logging.Logger;


//i18n import
import com.sun.enterprise.util.i18n.StringManager;

/**
 * Config Element Change Helper - class providing ElementChangeEvent
 * related services (events constructing/ change lists filtering etc.
 * 
 */
public class ElementChangeHelper{

    /**
     * Event type
     */
    static final String eventType = ElementChangeHelper.class.getName();

    // i18n StringManager
    private static StringManager localStrings = StringManager.getManager( ElementChangeHelper.class );
    private static String PROPERTY_SUBSTR = "/"+ServerTags.ELEMENT_PROPERTY+"[";

    private static final Logger logger = Logger.getLogger(AdminConstants.kLoggerName);
    
    //**********************************************************************
    public ElementChangeHelper() 
    {
    }

    //**********************************************************************
    static boolean isChangesToMerge(ConfigChange change1, ConfigChange change2)
    {
        String xpath1, xpath2;
        if( change1==null || change2==null ||
            (xpath1 = change1.getXPath())==null ||
            (xpath2 = change2.getXPath())==null ||
            !compareXPathes( xpath1, xpath2) )
            return false;
        if(isPropertyXPath(xpath2)) //optimize it later
            return true;
        return ( xpath1.equals(change2.getXPath()) &&
            change1.getClass().equals(change2.getClass()) );
    }
    
    //***************************************************************************
    static boolean checkChangeListForElement(ArrayList list)
    {
        if(list==null || list.size()<=0)
            return false;
        for(int i=1; i<list.size(); i++)
            if(!isChangesToMerge((ConfigChange)list.get(i-1), (ConfigChange)list.get(i))) 
               return false;
        return true;
    }
    //***************************************************************************
    static boolean isPropertyChange(ConfigChange change)
    {
        String xpath;
        return (change!=null &&
                (xpath = change.getXPath())!=null &&
                (xpath.indexOf(PROPERTY_SUBSTR))>=0 );
    }
    //***************************************************************************
    static boolean isPropertyXPath(String xpath)
    {
        return ((xpath != null) && ((xpath.indexOf(PROPERTY_SUBSTR))>=0));
    }
    //***************************************************************************
    // getting the primary key of config element
    static String getConfigElementPrimaryKey(String xpath)
    {
        if (xpath != null) {
            xpath = xpath.trim();
        }
        if((xpath!= null) && (xpath.endsWith("']")))
        {
            int idx = xpath.lastIndexOf('\'', xpath.length()-3);
            return xpath.substring(idx+1, xpath.length()-2);
        }
        return null;
    }    

    //***************************************************************************
    // getting the type of config element
    static String getConfigElementType(String xpath)
    {
        if(xpath.trim().endsWith("]"))
            xpath =  xpath.substring(0, xpath.lastIndexOf('['));
        return xpath.substring(0, xpath.lastIndexOf('/'));
    }    

    //***************************************************************************
    // get target name for event
    public static String getConfigElementTargetName(String xpath, ConfigContext ctx)
    {
        String targetName = null;
        try {
            // Get target name for event
            TargetBuilder targetBuilder = new TargetBuilder();
            return targetBuilder.getTargetNameForXPath(xpath, ctx, true);
        } catch(Exception e)
        {
            //can not find target -> get default (domain)
        }
        return null;
    }
    //***************************************************************************
    static String getElementXPath(ArrayList changes)
    {
        return getElementXPath((ConfigChange)changes.get(0));
    }
    
    public static String getElementXPath(ConfigChange change)
    {
        String xpath = change.getXPath();
        if(xpath==null)
            return xpath;
        int iMatch = xpath.indexOf(PROPERTY_SUBSTR);
        if(iMatch<0)
            return xpath;
        return xpath.substring(0, iMatch); 
    }
    //***************************************************************************
    static int getActionCodeForChanges(ArrayList changes)
    {
        return getActionCodeForChange((ConfigChange)changes.get(0));
    }
    
    static int getActionCodeForChange(ConfigChange change)
    {
        if(isPropertyXPath(change.getXPath()))
            return ElementChangeEvent.ACTION_ELEMENT_UPDATE;
        int action;
	//set action code
        if(change instanceof ConfigSet)
            action = ElementChangeEvent.ACTION_ELEMENT_CREATE;
        else if(change instanceof ConfigAdd)
            action = ElementChangeEvent.ACTION_ELEMENT_CREATE;
        else if(change instanceof ConfigUpdate)
            action = ElementChangeEvent.ACTION_ELEMENT_UPDATE;
        else if(change instanceof ConfigDelete)
            action = ElementChangeEvent.ACTION_ELEMENT_DELETE;
        else
            action = ElementChangeEvent.ACTION_ELEMENT_UNDEFINED;
        return action;
    }
    
    //***************************************************************************
    static private boolean compareXPathes(String xpath1, String xpath2)
    {
        int iProp;
        if((iProp = xpath1.indexOf(PROPERTY_SUBSTR))>0)
            xpath1 = xpath1.substring(0, iProp);
        if((iProp = xpath2.indexOf(PROPERTY_SUBSTR))>0)
            xpath2 = xpath2.substring(0, iProp);
        return xpath1.equals(xpath2); 
    }

    /* 
     * generate the array of ElementChangeEvents from given change list
     * @param changeList list of ConfigChanges to analyse
     * @param domainContext ConfigContext of domaiin.xml config file
     * @returns array of ElementChangeEvents
     * @throws IllegalArgumentException
     */
    public AdminEvent[] generateElementChangeEventsFromChangeList(String instanceName, ArrayList changeList, ConfigContext domainContext)
    {
        ArrayList merged = new ArrayList();
        ArrayList events = new ArrayList();
        ConfigChange change, lastChange = null;
        for(int i=0; i<changeList.size(); i++)
        {
            change = (ConfigChange)changeList.get(i);

            // check if dynamic reconfig is needed for this config change
            if (! isChangeDynamicReconfigNeeded(change)) {
                break;
            }

            if(merged.isEmpty() || 
               isChangesToMerge(lastChange, change))
            {
                //add change to list
                merged.add(change);
                lastChange = change;
            }
            else
            {
                //here we are only if collected changes portion completed
                // it is time to create ElementChangeEvent 
                if(!merged.isEmpty())
                {
                    ArrayList new_events = createEventsForElementChange(instanceName, merged, domainContext, changeList);
                    if(new_events!=null && new_events.size()>0)
                        events.addAll(new_events);
                    //FIXME: - do we need treat empty answer as "restart is needed" sign?
                    merged.clear();
                    //add change to list
                    merged.add(change);
                    lastChange = change;
                }
            }
        }
        // last portion could be here
        if(!merged.isEmpty())
        {
            ArrayList new_events = createEventsForElementChange(instanceName, merged, domainContext, changeList);
            if(new_events!=null && new_events.size()>0)
                events.addAll(new_events);
            //FIXME: - do we need treat empty answer as "restart is needed" sign?
        }
        return (AdminEvent[])events.toArray(new AdminEvent[events.size()]);   
    }

    /* 
     * create ElementChangeEvents from given change list 
     *    (all changes should be related to the same element)
     * @param changeList list of ConfigChanges related to the same element
     * @param domainContext ConfigContext of domaiin.xml config file
     * @returns array of ElementChangeEvent (or null)
     * @throws IllegalArgumentException
     */
    public ArrayList createEventsForElementChange(String instanceName, ArrayList changeList, ConfigContext domainContext, ArrayList globalChangeList)
    {
       return createEventsForElementChange(instanceName, changeList, domainContext, true, globalChangeList);
    }
    
    private ArrayList createEventsForElementChange(String instanceName, ArrayList changeList, ConfigContext domainContext, boolean bCheckList, ArrayList globalChangeList)
    {
        
        if(bCheckList && !checkChangeListForElement(changeList))
        {
            String msg = localStrings.getString( "admin.event.wrong_configchange" );
            throw new IllegalArgumentException( msg );
        }

        String xpath0  = getElementXPath(changeList);
        if(xpath0==null)
            return null;
        // instantiate the proper Event
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // 1. get event name

        MBeanRegistryEntry  entry = getRegistryEntry(xpath0);
        if(entry==null)
            return null;

        String              eventName = entry.getElementChangeEventName();
        if(eventName!=null && eventName.indexOf('.')<0)
            eventName = EVENT_PACKAGE_PATH_PREFIX + eventName;
        if(eventName==null)
            return null; //element is not a subject for ElementChangeEvent

        //  2. instantiate event
        try {
           return ElementChangeEventsFactory.createEvents(eventName, instanceName, 
                      entry.getName(), changeList, domainContext, globalChangeList);
        } catch(Exception e)
        {
            e.printStackTrace();
            String msg = localStrings.getString( "admin.event.cannot_create_eventforchange", eventName, xpath0 );
            throw new IllegalArgumentException( msg );
        }
    }

    
    String              lastXpath = null;
    MBeanRegistryEntry  lastEntry = null;
    static final public String EVENT_PACKAGE_PATH_PREFIX = "com.sun.enterprise.admin.event.";
    
    private synchronized MBeanRegistryEntry getRegistryEntry(String xpath)
    {
        if(xpath.equals(lastXpath))
            return lastEntry; 
        MBeanRegistry       registry = MBeanRegistryFactory.getAdminMBeanRegistry();
        MBeanRegistryEntry  entry = registry.findMBeanRegistryEntryByXPath(xpath);
        if(entry!=null)
        {
            lastXpath = xpath;
            lastEntry = entry;
        }
        return entry;
    }

    static public Set getXPathesForDynamicallyNotReconfigurableElements(ArrayList changeList)
    {
        HashSet xpathes = new HashSet();
        if(changeList==null)
            return xpathes;

        MBeanRegistry       registry = MBeanRegistryFactory.getAdminMBeanRegistry();
        String xpath_last = null;
        MBeanRegistryEntry  entry = null;
        boolean bLastEntryHasEvent = false;
        String propertyName = null;
        
        for(int i=0; i<changeList.size();i++)
        {
            ConfigChange change = (ConfigChange)changeList.get(i);
            int action = getActionCodeForChange(change);
            if(action==ElementChangeEvent.ACTION_ELEMENT_UNDEFINED)
                continue; //??? not add/set/update/delete
            String xpath = change.getXPath();
            // Check if this xpath belongs to one of the excluded 
            // XPaths. Excluded xpaths include lb-configs, lb-config
            // etc. Where restart checks do not apply.
            if (  (xpath == null) || isXPathExcludedForRestartCheck(xpath) ) {
                continue;
            }
            if(!xpath.equals(xpath_last))
            {
                xpath_last = xpath;
                if(isPropertyXPath(change.getXPath()))
                {
                    propertyName = getConfigElementPrimaryKey(xpath);
                    xpath = getElementXPath(change);
                }
                else
                {
                    propertyName = null;
                }
                entry = registry.findMBeanRegistryEntryByXPath(xpath);
                if(entry==null)
                {
                    xpathes.add(xpath);
                    continue;
                }
                bLastEntryHasEvent = (entry.getElementChangeEventName()!=null);
            }
            if(bLastEntryHasEvent)
                continue;
            // check whether creation reconfigurable
            if( (action==ElementChangeEvent.ACTION_ELEMENT_CREATE &&
                 entry.isElementCreationDynamicallyReconfigurable()))
            {
                xpathes.add(xpath);
                continue;
            }
            // check whether deletion reconfigurable
            if( (action==ElementChangeEvent.ACTION_ELEMENT_DELETE &&
                 entry.isElementDeletionDynamicallyReconfigurable()))
            {
                xpathes.add(xpath);
                continue;
            }
            // check properties changes
            if(propertyName!=null)
            {
                if(!entry.isPropertyDynamicallyReconfigurable(propertyName))
                {
                    xpathes.add(xpath);
                    continue;
                }
            }
            if ( !(change instanceof ConfigUpdate))
            {
                xpathes.add(xpath);
                continue;
            }
            //here we are only for attributes
            Set attrs = ((ConfigUpdate)change).getAttributeSet();
            Iterator iter  = attrs.iterator();
            while(iter.hasNext())
            {
                String attr = (String)iter.next();
                // check if dynamic reconfig is needed for the attribute, bug# 6509963
                try {
                    if (! entry.isAttributeDynamicReconfigNeeded(attr)) 
                    {
                        break;
                    }
                } catch (MBeanMetaException mme)
                {
                	//field not found in registry
                }

                if(!entry.isAttributeDynamicallyReconfigurable(attr))
                {
                    xpathes.add(xpath);
                    break;
                }
            }
        }
        return xpathes;
    }

    /**
     * Get a boolean value corresponding to the string value in xml.
     * @param strValue the string representing a boolean
     * @return true if the string is "true", "yes", "on" or "1"
     */
    public static boolean getBooleanValue(String strValue) {
        boolean retval = false;
        if (strValue == null) {
            return retval;
        }
        if (strValue.equalsIgnoreCase("true")
                || strValue.equalsIgnoreCase("yes")
                || strValue.equalsIgnoreCase("on")
                || strValue.equalsIgnoreCase("1")) {
            retval = true;
        }
        return retval;
    }
    
    /**
     * Checks if an XPath is excluded for restart required check.
     * Restart required check is skipped for lb-configs and lb-config
     * elements.
     *
     * @param xpath     XPath of the config change
     * 
     * @return boolean  true - if restart required check to be ignored, false
     *                  otherwise
     */
    private static boolean isXPathExcludedForRestartCheck(String xpath) 
    {
        if (xpath == null) {
            return false;
        }
        
        RestartExtensionFeature ref = AdminService.getRestartExtensionFeature();
        String[] extRestartExcludeXPaths = null;
        ArrayList list = new ArrayList();
        if(ref != null)
        {
	    extRestartExcludeXPaths = ref.getRestartExcludedPaths();
	    if(extRestartExcludeXPaths != null) {
                for(String xPath : restartExcludeXPaths)
		    list.add(xPath);	
		
	        for(String extXpath : extRestartExcludeXPaths)
                    list.add(extXpath);

	        restartExcludeXPaths = (String[])list.toArray(new String[0]);
	    }
        }
        for(int excludeIdx =0; excludeIdx < restartExcludeXPaths.length; 
                excludeIdx++) {

            if ( xpath.startsWith(restartExcludeXPaths[excludeIdx]) ) {
                return true;
            }
        }
        return false;
    }

    /*
     * finds "enabled" update element in change-list and returns its new value (as Boolean)
     *  or null if not found
     */
    public static Boolean findEnabledChange(ArrayList changeList)
    {
        if(changeList==null)
            return null;
        for(int i=changeList.size()-1; i>=0; i--)
        {
            if ( changeList.get(i) instanceof ConfigUpdate) 
                {
                    ConfigUpdate update = (ConfigUpdate)changeList.get(i);
                    String enableStr = update.getNewValue(ServerTags.ENABLED);
                    if (enableStr != null) 
                        return new Boolean(ElementChangeHelper.getBooleanValue(enableStr));
                }
        }
        return null;
    }

    // remove enable config changes from the config change list
    public static ArrayList<ConfigChange> removeEnabledChanges(ArrayList changeList)
    {
        if(changeList==null)
            return null;
        
        ArrayList<ConfigChange> enableConfigChangeList = 
            new ArrayList<ConfigChange>();
        for(int i=changeList.size()-1; i>=0; i--)
        {
            if ( changeList.get(i) instanceof ConfigUpdate)
                {
                    ConfigUpdate update = (ConfigUpdate)changeList.get(i);
                    String enableStr = update.getNewValue(ServerTags.ENABLED);
                    if (enableStr != null) {
                        enableConfigChangeList.add(
                            (ConfigChange)changeList.remove(i));
                    }
                }
        }
        return enableConfigChangeList;
    }

    /**
     * Check if the given config change needs dynamic reconfiguration
     */
    private boolean isChangeDynamicReconfigNeeded(ConfigChange change) {
        boolean b = true;

        // if not update return
        if ( !(change instanceof ConfigUpdate)) return b;

        // get registry entry
        MBeanRegistryEntry  entry = getRegistryEntry(change.getXPath());
        if (entry == null) return b;

        // check if dynamic reconfig needed for the given attribute
        Set attrs = ((ConfigUpdate)change).getAttributeSet();
        if (attrs.isEmpty()) return b;

        Iterator<String> attrIter = attrs.iterator();
        String attrName = null;
        while(attrIter.hasNext()) {
            try {
                attrName = attrIter.next();
                b = entry.isAttributeDynamicReconfigNeeded(attrName);
                // if the dynamic reconfig needed is true for any of 
                // the attributes then return true
                if (b) return b; 
            } catch (MBeanMetaException mme)
            {
                logger.log(Level.INFO, "event.mbean_registry_entry.attribute.not.found", attrName);
            }
        }
        return b;
    }

    // PRIVATE VARIABLES 

    // The XPath prefixes are excluded during restart required check.
    // These XPaths belong to lb-configs and lb-config element

    private static String[] restartExcludeXPaths = { "/domain/lb-configs","/domain/load-balancers" };

}
