/*
 * 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.addressing.EndpointReference;
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.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.axis2.engine.ListenerManager;
import org.apache.axis2.i18n.Messages;
import org.apache.axis2.util.LoggingControl;
import org.apache.axis2.util.MetaDataEntry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.namespace.QName;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Map;

/**
 * Well this is never clearly defined, what it does or the life-cycle.
 * So do NOT use this as it might not live up to your expectation.
 */
public class ServiceContext extends AbstractContext 
    implements Externalizable, SafeSerializable {

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

    private static final String myClassName = "ServiceContext";

    /**
     * An ID which can be used to correlate operations on an instance of
     * this object in the log files
     */
    private String logCorrelationIDString = null;


    /**
     * @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 = 8265625275015738957L;

    /**
     * @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;


    public static final String SERVICE_OBJECT = "serviceObject";

    private EndpointReference targetEPR;
    private EndpointReference myEPR;

    private transient AxisService axisService;

    // the service group context is the same as the parent
    private transient ServiceGroupContext serviceGroupContext;

    private transient ConfigurationContext configContext;

    /**
     * Should we cache the last OperationContext?
     */
    private boolean cachingOperationContext;
    /**
     * A cache for the last OperationContext
     */
    private transient OperationContext lastOperationContext;

    //----------------------------------------------------------------
    // 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 AxisService metadata will be used during
     * activate to match up with an existing object
     */
    private transient MetaDataEntry metaAxisService = null;

    /**
     * The ServiceGroupContext object will be used during
     * activate to finish its restoration
     */
    private transient ServiceGroupContext metaParent = null;

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

    /**
     * Public constructor (only here because this class is Externalizable)
     */
    public ServiceContext() {
    }

    /**
     * Constructor (package access, should only be used by ServiceGroupContext)
     *
     * @param axisService the AxisService for which to create a context
     * @param serviceGroupContext the parent ServiceGroupContext
     */
    ServiceContext(AxisService axisService, ServiceGroupContext serviceGroupContext) {
        super(serviceGroupContext);
        this.serviceGroupContext = serviceGroupContext;
        this.axisService = axisService;
        this.configContext = (ConfigurationContext) parent.getParent();
    }

    public OperationContext createOperationContext(QName name) {
        AxisOperation axisOp = axisService.getOperation(name);
        return createOperationContext(axisOp);
    }

    public OperationContext createOperationContext(AxisOperation axisOp) {
        OperationContext ctx = new OperationContext(axisOp, this);
        configContext.contextCreated(ctx);
        return ctx;
    }

    public AxisService getAxisService() {
        checkActivateWarning("getAxisService");
        return axisService;
    }

    public ConfigurationContext getConfigurationContext() {
        checkActivateWarning("getConfigurationContext");
        return configContext;
    }

    public ServiceGroupContext getServiceGroupContext() {
        checkActivateWarning("getServiceGroupContext");
        return serviceGroupContext;
    }

    /**
     * To get the ERP for a given service , if the transport is present and not
     * running then it will add as a listener to ListenerManager , there it will
     * init that and start the listener , and finally ask the EPR from transport
     * for a given service
     *
     * @param transport : Name of the transport
     * @return
     * @throws AxisFault
     */
    public EndpointReference getMyEPR(String transport) throws AxisFault {
        axisService.isEnableAllTransports();
        ConfigurationContext configctx = this.configContext;
        if (configctx != null) {
            ListenerManager lm = configctx.getListenerManager();
            if (!lm.isListenerRunning(transport)) {
                TransportInDescription trsin =
                        configctx.getAxisConfiguration().getTransportIn(transport);
                if (trsin != null) {
                    lm.addListener(trsin, false);
                } else {
                    throw new AxisFault(Messages.getMessage("transportnotfound",
                                                            transport));
                }
            }
            if (!lm.isStopped()) {
                return lm.getEPRforService(axisService.getName(), null, transport);
            }
        }
        return null;
    }

    public EndpointReference getTargetEPR() {
        return targetEPR;
    }

    public void setTargetEPR(EndpointReference targetEPR) {
        this.targetEPR = targetEPR;
    }

    public EndpointReference getMyEPR() {
        if (myEPR == null) {
            try {
                if (ListenerManager.defaultConfigurationContext != null) {
                    ListenerManager listenerManager =
                            ListenerManager.defaultConfigurationContext.getListenerManager();
                    myEPR = listenerManager.getEPRforService(axisService.getName(), null, null);
                }
            } catch (AxisFault axisFault) {
                // what else I can do 
                myEPR = null;
            }
        }
        return myEPR;
    }

    public void setMyEPR(EndpointReference myEPR) {
        this.myEPR = myEPR;
    }

    public OperationContext getLastOperationContext() {
        return lastOperationContext;
    }

    public void setLastOperationContext(OperationContext lastOperationContext) {
        this.lastOperationContext = lastOperationContext;
    }

    public boolean isCachingOperationContext() {
        return cachingOperationContext;
    }

    public void setCachingOperationContext(boolean cacheLastOperationContext) {
        this.cachingOperationContext = cacheLastOperationContext;
    }


    /**
     * Returns a name associated with this ServiceContext.
     * <p/>
     * Note: this name is from the corresponding
     * AxisService object.
     *
     * @return The name string, or null if no name can be found
     */
    public String getName() {
        if (axisService != null) {
            return axisService.getName();
        }

        if (metaAxisService != null) {
            return metaAxisService.getName();
        }

        return null;
    }


    /**
     * Returns a name associated with the ServiceGroupContext
     * associated with this ServiceContext.
     *
     * @return The name string, or null if no name can be found
     */
    public String getGroupName() {
        if (serviceGroupContext != null) {
            return serviceGroupContext.getId();
        }

        if (metaParent != null) {
            return metaParent.getId();
        }

        return null;
    }

    /* ===============================================================
     * 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);
        //---------------------------------------------------------
        // 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());
        out.writeBoolean(cachingOperationContext);
        out.writeObject(getLogCorrelationIDString());

        // EndpointReference targetEPR
        out.writeObject(targetEPR);
       
        // EndpointReference myEPR
        out.writeObject(myEPR);
        
        //---------------------------------------------------------
        // properties
        //---------------------------------------------------------
        out.writeMap(getProperties());
        
        //---------------------------------------------------------
        // AxisService
        //---------------------------------------------------------
        metaAxisService = null;
        if (axisService != null) {
            metaAxisService =
                    new MetaDataEntry(axisService.getClass().getName(), axisService.getName());
        }
        out.writeObject(metaAxisService);

        //---------------------------------------------------------
        // parent 
        //---------------------------------------------------------
        out.writeObject(getParent());
    }


    /**
     * 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 (LoggingControl.debugLoggingAllowed && log.isTraceEnabled()) {
            log.trace(myClassName + ":readExternal():  BEGIN  bytes available in stream [" +
                in.available() + "]  ");
        }

        //---------------------------------------------------------
        // object level identifiers
        //---------------------------------------------------------

        // 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);
        cachingOperationContext = in.readBoolean();
        logCorrelationIDString = (String) in.readObject();

        // trace point
        if (LoggingControl.debugLoggingAllowed && log.isTraceEnabled()) {
            log.trace(myClassName + ":readExternal():  reading input stream for [" +
                      getLogCorrelationIDString()+ "]  ");
        }

        // EndpointReference targetEPR
        targetEPR = (EndpointReference) in.readObject();

        // EndpointReference myEPR
        myEPR = (EndpointReference) in.readObject();

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

        //---------------------------------------------------------
        // AxisService
        //---------------------------------------------------------

        // axisService is not usable until the meta data has been reconciled
        metaAxisService = (MetaDataEntry) in.readObject();

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

        // ServiceGroupContext is not usable until it has been activated 
        metaParent = (ServiceGroupContext) in.readObject();

        //---------------------------------------------------------
        // other
        //---------------------------------------------------------

        // currently not saving this object
        lastOperationContext = null;

        //---------------------------------------------------------
        // done
        //---------------------------------------------------------
    }


    /**
     * This method checks to see if additional work needs to be
     * done in order to complete the object reconstitution.
     * Some parts of the object restored from the readExternal()
     * cannot be completed until we have a configurationContext
     * from the active engine. The configurationContext is used
     * to help this object to plug back into the engine's
     * configuration and deployment objects.
     *
     * @param cc The configuration context object representing the active configuration
     */
    public void activate(ConfigurationContext cc) {
        // see if there's any work to do
        if (!needsToBeReconciled) {
            // return quick
            return;
        }

        // use the supplied configuration context
        configContext = cc;

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

        // We previously saved metaAxisService; restore it
        axisService = null;

        if (metaAxisService != null) {
            axisService = ActivateUtils.findService(axisConfig, metaAxisService.getClassName(),
                                                       metaAxisService.getQNameAsString());
        }

        // the parent ServiceGroupContext object was saved
        // either use the restored object or sync up with 
        // an existing ServiceGroupContext object
        if (metaParent != null) {
            // find out if a copy of the ServiceGroupContext object exists on this
            // engine where this ServiceContext is being restored/activated
            // if so, use that object instead of the restored object
            // in order to make sure that future changes to group-level 
            // properties are preserved for future operations
            String groupName = metaParent.getId();

            ServiceGroupContext existingSGC = cc.getServiceGroupContext(groupName);

            if (existingSGC == null) {
                // could not find an existing ServiceGroupContext
                // use the restored object
                metaParent.activate(cc);

                // set parent 
                this.setParent(metaParent);
            } else {
                // switch over to the existing object
                this.setParent(existingSGC);

                // do a copy of the properties from the restored object
                // to the existing ServiceContext
                // Should the copy be a non-destructive one?  That is,
                // if the key already exists in the properties table of the
                // existing object, should the value be overwritten from the 
                // restored ojbect? For now, the decision is that the state
                // that has been preserved for a saved context object is
                // is important to be restored.
                metaParent.putContextProperties(existingSGC);
            }

        } else {
            // set parent to null
            this.setParent(metaParent);
        }

        // this is another pointer to our parent object
        serviceGroupContext = (ServiceGroupContext) this.getParent();

        if (serviceGroupContext != null) {
            // add the service context to the table
            serviceGroupContext.addServiceContext(this);
        }

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

        // make sure this restored object is in the parent's list
        if (metaParent != null) {
            // make sure this restored object is in the parent's list
            metaParent.addServiceContext(this);
        }

    }

    /**
     * This will do a copy of the properties from this context object
     * to the properties of the specified context object.
     *
     * @param context            The ServiceContext object to hold the merged properties
     * @param doParentProperties Indicates whether to go up the context hierachy
     *                           copy the properties at each level
     */
    public void putContextProperties(ServiceContext context, boolean doParentProperties) {
        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);

            if (doParentProperties) {
                ServiceGroupContext mySGC = null;

                if (serviceGroupContext != null) {
                    mySGC = serviceGroupContext;
                } else if (metaParent != null) {
                    mySGC = metaParent;
                }

                if (mySGC != null) {
                    ServiceGroupContext sgc = context.getServiceGroupContext();
                    mySGC.putContextProperties(sgc);
                }
            }
        }
    }


    /**
     * 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
     * @return TRUE if this object is equivalent with the specified object
     *         that is, key fields match
     *         FALSE, otherwise
     */
    public boolean isEquivalent(ServiceContext ctx) {
        // NOTE: the input object is expected to exist (ie, be non-null)

        if (!this.axisService.equals(ctx.getAxisService())) {
            return false;
        }


        EndpointReference targetEPR2 = ctx.getTargetEPR();

        if ((this.targetEPR != null) && (targetEPR2 != null)) {
            if (!this.targetEPR.isEquivalent(targetEPR2)) {
                return false;
            }
        } else if ((this.targetEPR == null) && (targetEPR2 == null)) {
            // keep going
        } else {
            // one of the objects is null
            return false;
        }

        EndpointReference myEPR2 = ctx.getMyEPR();

        if ((this.myEPR != null) && (myEPR2 != null)) {
            if (!this.myEPR.isEquivalent(myEPR2)) {
                return false;
            }
        } else if ((this.myEPR == null) && (myEPR2 == null)) {
            // keep going
        } else {
            // one of the objects is null
            return false;
        }

        // TODO: consider checking the parent objects for equivalency

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

        return true;
    }


    /**
     * Get the ID associated with this object instance.
     *
     * @return A string that can be output to a log file as an identifier
     *         for this object instance.  It is suitable for matching related log
     *         entries.
     */
    public String getLogCorrelationIDString() {
        if (logCorrelationIDString == null) {
            logCorrelationIDString = myClassName + "@" + UUIDGenerator.getUUID();
        }
        return logCorrelationIDString;
    }


    /**
     * 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) {
            if (LoggingControl.debugLoggingAllowed && log.isDebugEnabled()) {
                log.debug(getLogCorrelationIDString()+ ":" + methodname + "(): ****WARNING**** "
                        + myClassName + ".activate(configurationContext) needs to be invoked.");
            }
        }
    }

    public ConfigurationContext getRootContext() {
        return configContext;
    }


}
