/*
 * Copyright 1999-2004 The Apache Software Foundation.
 * 
 * Licensed 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.sandesha2.msgprocessors;

import java.util.Collection;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAP11Constants;
import org.apache.axiom.soap.SOAP12Constants;
import org.apache.axis2.AxisFault;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.OperationContext;
import org.apache.axis2.engine.AxisEngine;
import org.apache.axis2.transport.TransportUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sandesha2.RMMsgContext;
import org.apache.sandesha2.Sandesha2Constants;
import org.apache.sandesha2.SandeshaException;
import org.apache.sandesha2.client.SandeshaClientConstants;
import org.apache.sandesha2.client.SandeshaListener;
import org.apache.sandesha2.i18n.SandeshaMessageHelper;
import org.apache.sandesha2.i18n.SandeshaMessageKeys;
import org.apache.sandesha2.security.SecurityManager;
import org.apache.sandesha2.security.SecurityToken;
import org.apache.sandesha2.storage.StorageManager;
import org.apache.sandesha2.storage.Transaction;
import org.apache.sandesha2.storage.beanmanagers.RMDBeanMgr;
import org.apache.sandesha2.storage.beans.RMDBean;
import org.apache.sandesha2.storage.beans.RMSBean;
import org.apache.sandesha2.util.FaultManager;
import org.apache.sandesha2.util.MsgInitializer;
import org.apache.sandesha2.util.RMMsgCreator;
import org.apache.sandesha2.util.RangeString;
import org.apache.sandesha2.util.SandeshaUtil;
import org.apache.sandesha2.util.SequenceManager;
import org.apache.sandesha2.util.TerminateManager;
import org.apache.sandesha2.wsrm.Accept;
import org.apache.sandesha2.wsrm.CreateSequence;
import org.apache.sandesha2.wsrm.CreateSequenceResponse;
import org.apache.sandesha2.wsrm.Endpoint;
import org.apache.sandesha2.wsrm.SequenceOffer;

/**
 * Responsible for processing an incoming Create Sequence message.
 */

public class CreateSeqMsgProcessor implements MsgProcessor {

	private static final Log log = LogFactory.getLog(CreateSeqMsgProcessor.class);

	public boolean processInMessage(RMMsgContext createSeqRMMsg, Transaction transaction) throws AxisFault {
		if (log.isDebugEnabled())
			log.debug("Enter: CreateSeqMsgProcessor::processInMessage");

		try {
			
			CreateSequence createSeqPart = (CreateSequence) createSeqRMMsg
					.getMessagePart(Sandesha2Constants.MessageParts.CREATE_SEQ);
			if (createSeqPart == null) {
				if (log.isDebugEnabled())
					log.debug(SandeshaMessageHelper.getMessage(SandeshaMessageKeys.noCreateSeqParts));
				FaultManager.makeCreateSequenceRefusedFault(createSeqRMMsg, 
																										SandeshaMessageHelper.getMessage(SandeshaMessageKeys.noCreateSeqParts), 
																										new Exception());
				// Return false if an Exception hasn't been thrown.
				if (log.isDebugEnabled())
					log.debug("Exit: CreateSeqMsgProcessor::processInMessage " + Boolean.FALSE);				
				return false;

			}
	
			MessageContext createSeqMsg = createSeqRMMsg.getMessageContext();
			ConfigurationContext context = createSeqMsg.getConfigurationContext();
			StorageManager storageManager = SandeshaUtil.getSandeshaStorageManager(context, context.getAxisConfiguration());
			
			// If the inbound CreateSequence includes a SecurityTokenReference then
			// ask the security manager to resolve that to a token for us. We also
			// check that the Create was secured using the token.
			SecurityManager secManager = SandeshaUtil.getSecurityManager(context);
			OMElement theSTR = createSeqPart.getSecurityTokenReference();
			SecurityToken token = null;
			if(theSTR != null) {
				MessageContext msgcontext = createSeqRMMsg.getMessageContext();
				token = secManager.getSecurityToken(theSTR, msgcontext);
				
				// The create must be the body part of this message, so we check the
				// security of that element.
				OMElement body = msgcontext.getEnvelope().getBody();
				secManager.checkProofOfPossession(token, body, msgcontext);
			}
	
			//if toAddress is RMAnon we may need to terminate the request side sequence here.
			EndpointReference toEPR = createSeqMsg.getTo();
			if (toEPR.hasAnonymousAddress()) {
	
				RMSBean findBean = new RMSBean ();
				findBean.setReplyToEPR(toEPR.getAddress());
				findBean.setTerminationPauserForCS(true);
				
				//TODO recheck
				RMSBean rmsBean = storageManager.getRMSBeanMgr().findUnique(findBean);
				if (rmsBean!=null) {					
					//AckManager hs not done the termination. Do the termination here.
					MessageContext requestSideRefMessage = storageManager.retrieveMessageContext(rmsBean.getReferenceMessageStoreKey(),context);
					if (requestSideRefMessage==null) {
						FaultManager.makeCreateSequenceRefusedFault(createSeqRMMsg, 
								SandeshaMessageHelper.getMessage(SandeshaMessageKeys.referencedMessageNotFound, rmsBean.getInternalSequenceID()),
								new Exception());						
						// Return false if an Exception hasn't been thrown.
						if (log.isDebugEnabled())
							log.debug("Exit: CreateSeqMsgProcessor::processInMessage " + Boolean.FALSE);				
						return false;
					}
					
					RMMsgContext requestSideRefRMMessage = MsgInitializer.initializeMessage(requestSideRefMessage);
					TerminateManager.addTerminateSequenceMessage(requestSideRefRMMessage, rmsBean.getInternalSequenceID(), rmsBean.getSequenceID(), storageManager);
				}
			}
				

			MessageContext outMessage = null;
	
			// Create the new sequence id, as well as establishing the beans that handle the
			// sequence state.
			RMDBean rmdBean = SequenceManager.setupNewSequence(createSeqRMMsg, storageManager, secManager, token);
				
			RMMsgContext createSeqResponse = RMMsgCreator.createCreateSeqResponseMsg(createSeqRMMsg, rmdBean);
			outMessage = createSeqResponse.getMessageContext();
			// Set a message ID for this Create Sequence Response message
			outMessage.setMessageID(SandeshaUtil.getUUID());
				
			createSeqResponse.setFlow(MessageContext.OUT_FLOW);
	
			// for making sure that this won't be processed again
			createSeqResponse.setProperty(Sandesha2Constants.APPLICATION_PROCESSING_DONE, "true"); 
			
			CreateSequenceResponse createSeqResPart = (CreateSequenceResponse) createSeqResponse
					.getMessagePart(Sandesha2Constants.MessageParts.CREATE_SEQ_RESPONSE);
	
				// OFFER PROCESSING
			SequenceOffer offer = createSeqPart.getSequenceOffer();
			if (offer != null) {
				Accept accept = createSeqResPart.getAccept();
				if (accept == null) {
					if (log.isDebugEnabled())
						log.debug(SandeshaMessageHelper.getMessage(SandeshaMessageKeys.noAcceptPart));
					FaultManager.makeCreateSequenceRefusedFault(createSeqRMMsg, 
																											SandeshaMessageHelper.getMessage(SandeshaMessageKeys.noAcceptPart), 
																											new Exception());
					// Return false if an Exception hasn't been thrown.
					if (log.isDebugEnabled())
						log.debug("Exit: CreateSeqMsgProcessor::processInMessage " + Boolean.FALSE);				
					return false;
				}
	
				// offered seq id
				String offeredSequenceID = offer.getIdentifer().getIdentifier(); 
				
				boolean offerEcepted = offerAccepted(offeredSequenceID, context, createSeqRMMsg, storageManager);
	
				if (offerEcepted) {
					// Setting the CreateSequence table entry for the outgoing
					// side.
					RMSBean rMSBean = new RMSBean();
					rMSBean.setSequenceID(offeredSequenceID);
					String outgoingSideInternalSequenceId = SandeshaUtil
							.getOutgoingSideInternalSequenceID(rmdBean.getSequenceID());
					rMSBean.setInternalSequenceID(outgoingSideInternalSequenceId);
					// this is a dummy value
					rMSBean.setCreateSeqMsgID(SandeshaUtil.getUUID()); 
						
					rMSBean.setToEPR(rmdBean.getToEPR());
					rMSBean.setAcksToEPR(rmdBean.getToEPR());  // The acks need to flow back into this endpoint
					rMSBean.setReplyToEPR(rmdBean.getReplyToEPR());
					rMSBean.setLastActivatedTime(System.currentTimeMillis());
					rMSBean.setRMVersion(rmdBean.getRMVersion());
					rMSBean.setClientCompletedMessages(new RangeString());
	
					// Setting sequence properties for the outgoing sequence.
					// Only will be used by the server side response path. Will
					// be wasted properties for the client side.
						
					Endpoint endpoint = offer.getEndpoint();
					if (endpoint!=null) {
						// setting the OfferedEndpoint
						rMSBean.setOfferedEndPoint(endpoint.getEPR().getAddress());
					}
		
					rmdBean.setOutboundInternalSequence(outgoingSideInternalSequenceId);
					RMDBeanMgr rmdBeanMgr = storageManager.getRMDBeanMgr();
					rmdBeanMgr.update(rmdBean);
	
					// Store the inbound token (if any) with the new sequence
					rMSBean.setSecurityTokenData(rmdBean.getSecurityTokenData());
					
					// If this new sequence has anonymous acksTo, then we must poll for the acks
					// If the inbound sequence is targetted at the WSRM anonymous URI, we need to start
					// polling for this sequence.
					String acksTo = rMSBean.getAcksToEPR();
					EndpointReference reference = new EndpointReference(acksTo);
					if ((acksTo == null || reference.hasAnonymousAddress()) &&
						Sandesha2Constants.SPEC_VERSIONS.v1_1.equals(createSeqRMMsg.getRMSpecVersion())) {
						rMSBean.setPollingMode(true);
					}
					
					// Set the SOAP Version for this sequence.
					rMSBean.setSoapVersion(SandeshaUtil.getSOAPVersion(createSeqRMMsg.getSOAPEnvelope()));
					
					if (isReplayModel(createSeqRMMsg)) {
						rMSBean.setReplayModel(true);
						rmdBean.setReplayModel(true);
					}
					
					storageManager.getRMSBeanMgr().insert(rMSBean);
					
					SandeshaUtil.startWorkersForSequence(context, rMSBean);
					
				} else {
					// removing the accept part.
					createSeqResPart.setAccept(null);
					createSeqResponse.addSOAPEnvelope();
				}
			}
							
			//TODO add createSequenceResponse message as the referenceMessage to the RMDBean.
	
			outMessage.setResponseWritten(true);
	
			rmdBean.setLastActivatedTime(System.currentTimeMillis());
			
			// If the inbound sequence is targetted at the anonymous URI, we need to start
			// polling for this sequence.
			if (toEPR.hasAnonymousAddress()) {
				if (Sandesha2Constants.SPEC_VERSIONS.v1_1.equals(createSeqRMMsg.getRMSpecVersion())) {
					rmdBean.setPollingMode(true);
				}
			}
			
			storageManager.getRMDBeanMgr().update(rmdBean);
	
			SandeshaUtil.startWorkersForSequence(context, rmdBean);

			
			try{
				AxisEngine.send(outMessage);				
			}
			catch(AxisFault e){
				FaultManager.makeCreateSequenceRefusedFault(createSeqRMMsg, 
						SandeshaMessageHelper.getMessage(SandeshaMessageKeys.couldNotSendCreateSeqResponse, 
																						 SandeshaUtil.getStackTraceFromException(e)), e);
				// Return false if an Exception hasn't been thrown.
				if (log.isDebugEnabled())
					log.debug("Exit: CreateSeqMsgProcessor::processInMessage " + Boolean.FALSE);				
				return false;
			}

			// We have done all of our updates, so commit the transaction
			if(transaction != null && transaction.isActive()) {
				transaction.commit();
			}
			
			EndpointReference replyTo = createSeqMsg.getReplyTo();
			if(replyTo == null || replyTo.hasAnonymousAddress()) {
				TransportUtils.setResponseWritten(createSeqMsg, true);
			}
			
	//		SequencePropertyBean findBean = new SequencePropertyBean ();
	//		findBean.setName (Sandesha2Constants.SequenceProperties.TERMINATE_ON_CREATE_SEQUENCE);
	//		findBean.setValue(createSeqMsg.getTo().getAddress());
			
			createSeqRMMsg.pause();
		}
		catch (Exception e) {
			if (log.isDebugEnabled())
				log.debug("Caught an exception processing CreateSequence message", e);
			// Does the message context already contain a fault ?
			// If it doesn't then we can add the CreateSequenceRefusedFault.
			if (createSeqRMMsg.getMessageContext().getProperty(SOAP12Constants.SOAP_FAULT_CODE_LOCAL_NAME) == null && 
					createSeqRMMsg.getMessageContext().getProperty(SOAP11Constants.SOAP_FAULT_CODE_LOCAL_NAME) == null) {
				// Add the fault details to the message
				FaultManager.makeCreateSequenceRefusedFault(createSeqRMMsg, SandeshaUtil.getStackTraceFromException(e), e);				
				
				// Return false if an Exception hasn't been thrown.
				if (log.isDebugEnabled())
					log.debug("Exit: CreateSeqMsgProcessor::processInMessage " + Boolean.FALSE);				
				return false;
			} 
				
			// If we are SOAP12 and we have already processed the fault - rethrow the exception
			if (createSeqRMMsg.getMessageContext().getProperty(SOAP12Constants.SOAP_FAULT_CODE_LOCAL_NAME) != null) {
					// throw the original exception
					if (e instanceof AxisFault)
						throw (AxisFault)e;
					 
					throw new SandeshaException(e);
			}
		}

		if (log.isDebugEnabled())
			log.debug("Exit: CreateSeqMsgProcessor::processInMessage " + Boolean.TRUE);
		return true;
	}

	private boolean offerAccepted(String sequenceId, ConfigurationContext configCtx, RMMsgContext createSeqRMMsg,
			StorageManager storageManager) throws SandeshaException {
		if (log.isDebugEnabled())
			log.debug("Enter: CreateSeqMsgProcessor::offerAccepted, " + sequenceId);

		if ("".equals(sequenceId)) {
			if (log.isDebugEnabled())
				log.debug("Exit: CreateSeqMsgProcessor::offerAccepted, " + false);
			return false;
		}

		RMSBean createSeqFindBean = new RMSBean();
		createSeqFindBean.setSequenceID(sequenceId);
		Collection arr = storageManager.getRMSBeanMgr().find(createSeqFindBean);

		if (arr.size() > 0) {
			if (log.isDebugEnabled())
				log.debug("Exit: CreateSeqMsgProcessor::offerAccepted, " + false);
			return false;
		}
		if (sequenceId.length() <= 1) {
			if (log.isDebugEnabled())
				log.debug("Exit: CreateSeqMsgProcessor::offerAccepted, " + false);
			return false; // Single character offers are NOT accepted.
		}

		if (log.isDebugEnabled())
			log.debug("Exit: CreateSeqMsgProcessor::offerAccepted, " + true);
		return true;
	}

	public boolean processOutMessage(RMMsgContext rmMsgCtx) {
		if (log.isDebugEnabled())
			log.debug("Enter: CreateSeqMsgProcessor::processOutMessage");

		MessageContext msgCtx = rmMsgCtx.getMessageContext();

		// adding the SANDESHA_LISTENER
		SandeshaListener faultCallback = (SandeshaListener) msgCtx.getOptions().getProperty(
				SandeshaClientConstants.SANDESHA_LISTENER);
		if (faultCallback != null) {
			OperationContext operationContext = msgCtx.getOperationContext();
			if (operationContext != null) {
				operationContext.setProperty(SandeshaClientConstants.SANDESHA_LISTENER, faultCallback);
			}
		}
		if (log.isDebugEnabled())
			log.debug("Exit: CreateSeqMsgProcessor::processOutMessage " + Boolean.FALSE);
		return false;
	}
	
	/**
	 * Checks weather the created sequence belong to the reply model. For this following conditions has to be fulfilled.
	 * 
	 * 1. ReplyTo should be anonymous
	 * 2. There must be an offer for the response sequence.
	 * 3. RM Spec should be 1.0.
	 * 
	 * @param createSequenceMessage
	 * @return
	 * @throws SandeshaException
	 */
	private boolean isReplayModel (RMMsgContext createSequenceMessage) throws SandeshaException {
		boolean replayModel = false;
		
		CreateSequence createSequence = (CreateSequence) createSequenceMessage.getMessagePart(Sandesha2Constants.MessageParts.CREATE_SEQ);
		SequenceOffer offer = createSequence.getSequenceOffer();
		
		EndpointReference replyTo = createSequenceMessage.getReplyTo();
		
		String rmSpecVersion = createSequenceMessage.getRMSpecVersion();
		
		if (offer!=null &&
				replyTo!=null && replyTo.hasAnonymousAddress() &&
				rmSpecVersion!=null && rmSpecVersion.equals(Sandesha2Constants.SPEC_VERSIONS.v1_0))
			replayModel = true;
		
		return replayModel;
	}
	
}
