/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


package org.apache.axis2.context;

import org.apache.axiom.om.util.UUIDGenerator;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.externalize.ActivateUtils;
import org.apache.axis2.context.externalize.ExternalizeConstants;
import org.apache.axis2.context.externalize.SafeObjectInputStream;
import org.apache.axis2.context.externalize.SafeObjectOutputStream;
import org.apache.axis2.context.externalize.SafeSerializable;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.AxisServiceGroup;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.axis2.i18n.Messages;
import org.apache.axis2.util.MetaDataEntry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class ServiceGroupContext extends AbstractContext 
    implements Externalizable, SafeSerializable {

    /*
     * setup for logging
     */
    private static final Log log = LogFactory.getLog(ServiceGroupContext.class);

    private static final String myClassName = "ServiceGroupContext";

    /**
     * @serial The serialization version ID tracks the version of the class.
     * If a class definition changes, then the serialization/externalization
     * of the class is affected. If a change to the class is made which is
     * not compatible with the serialization/externalization of the class,
     * then the serialization version ID should be updated.
     * Refer to the "serialVer" utility to compute a serialization
     * version ID.
     */
    private static final long serialVersionUID = 9014471144479928885L;

    /**
     * @serial Tracks the revision level of a class to identify changes to the
     * class definition that are compatible to serialization/externalization.
     * If a class definition changes, then the serialization/externalization
     * of the class is affected.
     * Refer to the writeExternal() and readExternal() methods.
     */
    // supported revision levels, add a new level to manage compatible changes
    private static final int REVISION_2 = 2;
    // current revision level of this object
    private static final int revisionID = REVISION_2;


    private transient AxisServiceGroup axisServiceGroup;
    private String id;
    private Map<String, ServiceContext> serviceContextMap;

    //----------------------------------------------------------------
    // MetaData for data to be restored in activate after readExternal
    //----------------------------------------------------------------

    /**
     * Indicates whether the message context has been reconstituted
     * and needs to have its object references reconciled
     */
    private transient boolean needsToBeReconciled = false;


    /**
     * The AxisServiceContext metadata will be used during
     * activate to match up with an existing object
     */
    private transient MetaDataEntry metaAxisServiceGroup = null;

    //----------------------------------------------------------------
    // end MetaData section
    //----------------------------------------------------------------

    // simple constructor
    public ServiceGroupContext() {
        super(null);
        serviceContextMap = new HashMap<String, ServiceContext>();
    }


    public ServiceGroupContext(ConfigurationContext parent, AxisServiceGroup axisServiceGroup) {
        super(parent);
        this.axisServiceGroup = axisServiceGroup;
        serviceContextMap = new HashMap<String, ServiceContext>();
        // initially set the id to the axisServiceGroup
        if (axisServiceGroup != null) {
            setId(axisServiceGroup.getServiceGroupName());
        }
    }

    public AxisServiceGroup getDescription() {
        checkActivateWarning("getDescription");
        return axisServiceGroup;
    }

    public String getId() {
        return id;
    }

    /**
     * Gets a service context. Creates a new one from AxisService.
     * There is no need to store service context inside serviceGroup
     * context as well.
     *
     * @param service the AxisService for which to get a context
     * @return Returns ServiceContext.
     * @throws AxisFault if something goes wrong
     */
    public ServiceContext getServiceContext(AxisService service) throws AxisFault {
        AxisService axisService = axisServiceGroup.getService(service.getName());
        if (axisService == null) {
            throw new AxisFault(Messages.getMessage("invalidserviceinagroup",
                                                    service.getName(),
                                                    axisServiceGroup.getServiceGroupName()));
        }
        if (serviceContextMap == null) {
            serviceContextMap = new HashMap<String, ServiceContext>();
        }
        ServiceContext serviceContext = (ServiceContext) serviceContextMap.get(service.getName());
        if (serviceContext == null) {
            serviceContext = new ServiceContext(service, this);
            getRootContext().contextCreated(serviceContext);
            serviceContextMap.put(service.getName(), serviceContext);
        }
        return serviceContext;
    }

    public Iterator<ServiceContext> getServiceContexts() {
        if (serviceContextMap == null) {
            serviceContextMap = new HashMap<String, ServiceContext>();
        }
        if (serviceContextMap.isEmpty()) {
            return null;
        }
        return serviceContextMap.values().iterator();
    }

    public void setId(String id) {
        this.id = id;
    }

    /**
     * Adds the specified service context object to the
     * lists of service contexts for this service group
     * context.
     *
     * @param srvctx The ServiceContext object to add
     */
    public void addServiceContext(ServiceContext srvctx) {
        if (srvctx == null) {
            return;
        }

        AxisService axisService = srvctx.getAxisService();

        if (axisService == null) {
            return;
        }

        if (serviceContextMap == null) {
            serviceContextMap = new HashMap<String, ServiceContext>();
        }

        serviceContextMap.put(axisService.getName(), srvctx);

    }


    /**
     * Finds the service context object that corresponds
     * to the specified name from the list
     * of service contexts for this service group
     * context.
     *
     * @param name The name associated with the ServiceContext
     * @return The ServiceContext associated with the name,
     *         or null, if none can be found
     */
    public ServiceContext findServiceContext(String name) {
        if (serviceContextMap == null) {
            return null;
        }

        return (ServiceContext) serviceContextMap.get(name);
    }


    /**
     * Finds the service context object that corresponds
     * to the specified AxisService from the list
     * of service contexts for this service group
     * context.
     *
     * @param axisSrv the AxisService whose context we're looking for
     * @return The ServiceContext associated with the AxisService
     *         or null, if none can be found
     */
    public ServiceContext findServiceContext(AxisService axisSrv) {
        if (axisSrv == null) {
            return null;
        }

        if (serviceContextMap == null) {
            return null;
        }

        return (ServiceContext) serviceContextMap.get(axisSrv.getName());
    }

    /**
     * This will do a copy of the properties from this context object
     * to the properties of the specified context object.
     *
     * @param context The ServiceGroupContext object to hold the merged properties
     */
    public void putContextProperties(ServiceGroupContext context) {
        if (context != null) {
            // get the current properties on this context object
            Map<String, Object> props = getProperties();

            // copy them to the specified context object
            context.mergeProperties(props);
        }
    }

    /* ===============================================================
    * Externalizable support
    * ===============================================================
    */


    /**
     * Save the contents of this object.
     * <p/>
     * NOTE: Transient fields and static fields are not saved.
     * Also, objects that represent "static" data are
     * not saved, except for enough information to be
     * able to find matching objects when the message
     * context is re-constituted.
     *
     * @param out The stream to write the object contents to
     * @throws IOException
     */
    public void writeExternal(ObjectOutput o) throws IOException {
        SafeObjectOutputStream out = SafeObjectOutputStream.install(o);
        // write out contents of this object

        //---------------------------------------------------------
        // in order to handle future changes to the message 
        // context definition, be sure to maintain the 
        // object level identifiers
        //---------------------------------------------------------
        // serialization version ID
        out.writeLong(serialVersionUID);

        // revision ID
        out.writeInt(revisionID);

        //---------------------------------------------------------
        // various simple fields
        //---------------------------------------------------------

        out.writeLong(getLastTouchedTime());

        if (id == null) {
            // generate an ID to use when this object is restored
            id = UUIDGenerator.getUUID();
        }
        out.writeObject(id);

        //---------------------------------------------------------
        // properties
        //---------------------------------------------------------
        out.writeMap(getProperties());

        //---------------------------------------------------------
        // AxisServiceGroup
        //---------------------------------------------------------
        metaAxisServiceGroup = null;
        if (axisServiceGroup != null) {
            metaAxisServiceGroup = new MetaDataEntry(axisServiceGroup.getClass().getName(),
                                                     axisServiceGroup.getServiceGroupName());
        }
        out.writeObject(metaAxisServiceGroup);

        //---------------------------------------------------------
        // parent 
        //---------------------------------------------------------

        // the parent is the ConfigurationContext object, which
        // at this time, is not being saved.
        // instead, we will need to register this ServiceGroupObject
        // with the existing ConfigurationContext object when
        // this object is reconstituted

    }


    /**
     * Restore the contents of the object that was previously saved.
     * <p/>
     * NOTE: The field data must read back in the same order and type
     * as it was written.  Some data will need to be validated when
     * resurrected.
     *
     * @param in The stream to read the object contents from
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public void readExternal(ObjectInput inObject) throws IOException, ClassNotFoundException {
        SafeObjectInputStream in = SafeObjectInputStream.install(inObject);
        // set the flag to indicate that the message context is being
        // reconstituted and will need to have certain object references 
        // to be reconciled with the current engine setup
        needsToBeReconciled = true;

        // trace point
        if (log.isTraceEnabled()) {
            log.trace(myClassName + ":readExternal():  BEGIN  bytes available in stream [" +
                      in.available() + "]  ");
        }

        // serialization version ID
        long suid = in.readLong();

        // revision ID
        int revID = in.readInt();

        // make sure the object data is in a version we can handle
        if (suid != serialVersionUID) {
            throw new ClassNotFoundException(ExternalizeConstants.UNSUPPORTED_SUID);
        }

        // make sure the object data is in a revision level we can handle
        if (revID != REVISION_2) {
            throw new ClassNotFoundException(ExternalizeConstants.UNSUPPORTED_REVID);
        }

        //---------------------------------------------------------
        // various simple fields
        //---------------------------------------------------------

        long time = in.readLong();
        setLastTouchedTime(time);
        id = (String) in.readObject();

        //---------------------------------------------------------
        // properties
        //---------------------------------------------------------
        properties = in.readMap(new HashMapUpdateLockable());

        //---------------------------------------------------------
        // AxisServiceGroup
        //---------------------------------------------------------

        // axisServiceGroup is not usable until the meta data has been reconciled
        axisServiceGroup = null;
        metaAxisServiceGroup = (MetaDataEntry) in.readObject();

        //---------------------------------------------------------
        // parent 
        //---------------------------------------------------------

        // the parent is the ConfigurationContext object, whic
        // at this time, is not being saved.
        // instead, we will need to register this ServiceGroupObject
        // with the existing ConfigurationContext object when
        // this object is reconstituted

        //---------------------------------------------------------
        // other
        //---------------------------------------------------------
        serviceContextMap = new HashMap<String, ServiceContext>();

        //---------------------------------------------------------
        // done
        //---------------------------------------------------------

    }


    /**
     * Some parts of the object restored from the
     * readExternal deserialization work cannot be completed until
     * we have a configurationContext.  This method checks to see
     * if additional work needs to be done in order to complete
     * the object reconstitution.
     *
     * @param cc the active ConfigurationContext
     */
    public void activate(ConfigurationContext cc) {
        // see if there's any work to do
        if (!needsToBeReconciled) {
            // return quick
            return;
        }

        // get the axis configuration 
        AxisConfiguration axisConfig = cc.getAxisConfiguration();

        // We previously saved metaAxisServiceGroup; restore it
        if (metaAxisServiceGroup != null) {
            axisServiceGroup = 
                ActivateUtils.findServiceGroup(axisConfig,
                                               metaAxisServiceGroup.getClassName(),
                                               metaAxisServiceGroup.getQNameAsString());
        } else {
            axisServiceGroup = null;
        }

        // set parent 
        this.setParent(cc);

        // register with the parent
        cc.addServiceGroupContextIntoSoapSessionTable(this);

        //-------------------------------------------------------
        // done, reset the flag
        //-------------------------------------------------------
        needsToBeReconciled = false;

    }

    /**
     * Compares key parts of the state from the current instance of
     * this class with the specified instance to see if they are
     * equivalent.
     * <p/>
     * This differs from the java.lang.Object.equals() method in
     * that the equals() method generally looks at both the
     * object identity (location in memory) and the object state
     * (data).
     * <p/>
     *
     * @param ctx The object to compare with
     * @return TRUE if this object is equivalent with the specified object
     *         that is, key fields match
     *         FALSE, otherwise
     */
    public boolean isEquivalent(ServiceGroupContext ctx) {
        // NOTE: the input object is expected to exist (ie, be non-null)

        if (!this.axisServiceGroup.equals(ctx.getDescription())) {
            return false;
        }

        String ctxid = ctx.getId();

        if ((this.id != null) && (ctxid != null)) {
            if (!this.id.equals(ctxid)) {
                return false;
            }

        } else if ((this.id == null) && (ctxid == null)) {
            // keep going
        } else {
            // mismatch
            return false;
        }

        // TODO: consider checking the parent objects for equivalency

        // TODO: consider checking fields from the super class for equivalency

        return true;
    }

    /**
     * Trace a warning message, if needed, indicating that this
     * object needs to be activated before accessing certain fields.
     *
     * @param methodname The method where the warning occurs
     */
    private void checkActivateWarning(String methodname) {
        if (needsToBeReconciled) {
            log.warn(myClassName + ":" + methodname + "(): ****WARNING**** " + myClassName +
                     ".activate(configurationContext) needs to be invoked.");
        }
    }

    public ConfigurationContext getRootContext() {
        //parent of the ServiceGroupContext is the ConfigurationContext
        return (ConfigurationContext) this.getParent();
    }
}
