/*
 * $Id: ContextIDInterceptor.java,v 1.2 2012/02/27 22:26:03 agoubard Exp $
 *
 * See the COPYRIGHT file for redistribution and use restrictions.
 */
package org.xins.server;

import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.NDC;

import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.collections.InvalidPropertyValueException;
import org.xins.common.collections.MissingRequiredPropertyException;
import org.xins.common.manageable.BootstrapException;
import org.xins.common.manageable.InitializationException;
import org.xins.common.text.TextUtils;

/**
 * Interceptor for diagnostic context identifiers.
 * The key is generated by the ContextIDGenerator and set on the NDC by the ContextIDInterceptor.
 *
 * @see ContextIDGenerator
 * @since XINS 3.0
 * 
 * @version $Revision: 1.2 $ $Date: 2012/02/27 22:26:03 $
 * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
 */
public class ContextIDInterceptor extends Interceptor {

   private ContextIDGenerator contextIdGenerator;

   /**
    * Pattern which incoming diagnostic context identifiers must match. Can be
    * <code>null</code> in case no pattern has been specified. Initially this
    * field is indeed <code>null</code>.
    */
   private Pattern contextIDPattern;
   
   private boolean applyContextID;

   @Override
   protected void bootstrapImpl(Map<String, String> properties)
   throws MissingRequiredPropertyException,
          InvalidPropertyValueException,
          BootstrapException {
      contextIdGenerator = new ContextIDGenerator(getApi().getName());
      contextIdGenerator.bootstrap(properties);
   }
   
   @Override
   protected void initImpl(Map<String, String> properties)
   throws MissingRequiredPropertyException,
          InvalidPropertyValueException,
          InitializationException {

      // Determine filter for incoming diagnostic context IDs
      contextIDPattern = determineContextIDPattern(properties);
      
      contextIdGenerator.init(properties);
      
      applyContextID = !"false".equals(properties.get(ConfigManager.CONTEXT_ID_PUSH_PROPERTY));
   }

   @Override
   public HttpServletRequest beginRequest(HttpServletRequest httpRequest) {
      if (applyContextID) {
         applyContextID(httpRequest);
      }
      return httpRequest;
   }

   @Override
   public void endRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
      if (applyContextID) {
         NDC.pop();
         NDC.remove();
      }
   }

   /**
    * Applies an applicable diagnostic context identifier. 
    * If no diagnostic context identifier is specified or if the value is
    * invalid, a new one is created and applied.
    *
    * @param request
    *    the HTTP servlet request, should not be <code>null</code>.
    *
    * @return
    *    the diagnostic context identifier, never <code>null</code> and never
    *    an empty string.
    */
   private String applyContextID(HttpServletRequest request) {

      // See if the request already specifies a diagnostic context identifier
      // XXX: Store "_context" in a constant

      // Associate the current diagnostic context identifier with this thread
      String contextID = request.getParameter("_context");
      if (TextUtils.isEmpty(contextID)) {
         contextID = contextIdGenerator.generate();
         NDC.push(contextID);
         Log.log_3583(contextID);

      // Indeed there is a context ID in the request, make sure it's valid
      } else {

         // Valid context ID
         if (isValidContextID(contextID)) {
            NDC.push(contextID);
            Log.log_3581(contextID);

         // Invalid context ID
         } else {
            Log.log_3582(contextID);
            contextID = contextIdGenerator.generate();
            NDC.push(contextID);
            Log.log_3583(contextID);
         }
      }

      return contextID;
   }

   /**
    * Determines if the specified incoming context identifier is considered
    * valid.
    *
    * @param contextID
    *    the incoming diagnostic context identifier, should not be
    *    <code>null</code>.
    *
    * @return
    *    <code>true</code> if <code>contextID</code> is considered acceptable,
    *    <code>false</code> if it is considered unacceptable.
    */
   private boolean isValidContextID(String contextID) {

      // If a filter is specified, validate that the ID matches it
      if (contextIDPattern != null) {
         return contextIDPattern.matcher(contextID).matches();

      // No filter is specified, everything is allowed
      } else {
         return true;
      }
   }

   /**
    * Determines the filter for diagnostic context identifiers.
    *
    * @param properties
    *    the runtime properties to retrieve information from, cannot be
    *    <code>null</code>.
    *
    * @return
    *    the filter as a {@link Pattern} object, or <code>null</code> if no
    *    filter is specified.
    *
    * @throws IllegalArgumentException
    *    if <code>properties == null</code>.
    *
    * @throws InvalidPropertyValueException
    *    if the value for the filter property is considered invalid.
    */
   private Pattern determineContextIDPattern(Map<String, String> properties)
   throws IllegalArgumentException, InvalidPropertyValueException {

      // Check preconditions
      MandatoryArgumentChecker.check("properties", properties);

      // Determine pattern string
      // XXX: Store "org.xins.server.contextID.filter" in a constant
      String propName  = "org.xins.server.contextID.filter";
      String propValue = properties.get(propName);

      // If the property value is empty, then there is no pattern
      Pattern pattern;
      if (TextUtils.isEmpty(propValue)) {
         pattern = null;
         Log.log_3431();

      // Otherwise we must provide a Pattern instance
      } else {

         // Convert the string to a Pattern
         try {
            // XXX: Why is the pattern made case-insensitive?
            pattern = Pattern.compile(propValue, Pattern.CASE_INSENSITIVE);
            Log.log_3432(propValue);

         // Malformed pattern indicates an invalid value
         } catch (PatternSyntaxException exception) {
            Log.log_3433(propValue);
            InvalidPropertyValueException ipve;
            ipve = new InvalidPropertyValueException(propName, propValue);
            ipve.initCause(exception);
            throw ipve;
         }
      }

      return pattern;
   }
}