// SSIResource.java
// $Id: SSIResource.java,v 1.27 1997/07/28 12:49:57 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.ssi ;

import java.io.* ;
import java.util.* ;

import w3c.tools.store.*;
import w3c.www.http.* ;
import w3c.www.mime.*;
import w3c.jigsaw.resources.* ;
import w3c.jigsaw.http.* ;
import w3c.util.* ;

/**
 * This resource implements server-side parsing of HTML documents.
 * Any comment of the form <code>&lt;!--#commandName param1=val1
 * ... paramn=valn --&gt;</code> will be interpreted as an include
 * directive.
 * <p> Commands are looked up in an instance of the class
 * supplied in the registryClass attribute, which must be a subclass
 * of <code>w3c.jigsaw.ssi.CommandRegistry</code>.
 *
 * @author Antonio Ramirez <anto@mit.edu>
 * @author Benoit Mahe <bmahe@sophia.inria.fr>
 * @see w3c.jigsaw.ssi.CommandRegistry
 * @see w3c.jigsaw.ssi.Command
 */
  
public class SSIResource extends FileResource {

  static final boolean debug = false ;
    
  /** Attributes index - The segments */
  private static int ATTR_SEGMENTS = -1 ;
    
  /** Attributes index - The total unparsed size */
  private static int ATTR_UNPARSED_SIZE = -1 ;

  /** Attribute index - The class to use for making the CommandRegistry */
  private static int ATTR_REGISTRY_CLASS = -1 ;

  /** Attributes index - The maximum recursive parsing depth */
  private static int ATTR_MAX_DEPTH = -1 ;

  /** Attribute index - Whether or not to deny insecure commands */
  private static int ATTR_SECURE = -1 ;

  /**
   * The command registry used by the resource.
   */
  private CommandRegistry commReg = null ;

  /**
   * The most specific class of the current command registry.
   */
  private Class regClass = null ;
    
  /**
   * Here we keep track of created registries to avoid
   * making more than one per registry class.
   */
  private static Dictionary regs = new ArrayDictionary(5) ;

  /** Will hold the increments for finding "<!--#" */
  private static byte startIncrements[] = new byte[128] ;

  /** Same thing for "-->" */
  private static byte endIncrements[] = new byte[128] ;

  /** The start pattern */
  private static byte startPat[] =
  {
    (byte)'<',(byte)'!',(byte)'-',(byte)'-',(byte)'#'
  };

  /** The end pattern */
  private static byte endPat[] =
  {
    (byte)'-',(byte)'-',(byte)'>'
  };

  // For value-less parameters 
  private static final String emptyString = "" ;

  /**
   * Our "very global" variables
   */
  protected Dictionary vars = null;

  /**
   * Message state - the current recursion depth
   */
  public static final String STATE_DEPTH =
  "w3c.jigsaw.ssi.SSIResource.depth" ;

  /**
   * Message state - the current variables
   */
  public static final String STATE_VARIABLES =
  "w3c.jigsaw.ssi.SSIResource.variables" ;
  /**
   * Updates the attributes and instance variables that depend
   * on the file.
   */
  public void updateFileAttributes()
  {
    super.updateFileAttributes() ;
    parseFirstTime() ;
  }

  private final int getUnparsedSize()
  {
    return getInt(ATTR_UNPARSED_SIZE,-1) ;
  }

  private final void setUnparsedSize(int unparsedSize)
  {
    setValue(ATTR_UNPARSED_SIZE,new Integer(unparsedSize)) ;
  }

  /**
   * Makes sure that checkContent() is called on _any_ HTTP method,
   * so that the internal representation of commands is always consistent.
   * @param request The HTTPRequest
   * @param filters The filters to apply
   */

  public Reply perform(Request request)
    throws HTTPException, ClientException
  {
    checkContent() ;
    return super.perform(request) ;
  }

  // FIXME: doc

  public Reply get(Request request)
    throws HTTPException
  {
    Reply reply = handle(request) ;
    return reply != null
      ? reply
      : super.get(request) ;
  }

  // FIXME: doc
  public Reply post(Request request)
    throws HTTPException, ClientException
  {
    Reply reply = handle(request) ;
    return reply != null
      ? reply
      : super.post(request) ;
  }

  /**
   * Handles all relevant HTTP methods.
   * Merges the partial replies from each of the segments into
   * one global reply.
   * <strong>Remark</strong>: no direct relation to PostableResource.handle()
   * @param request The HTTP request
   */

  public Reply handle(Request request)
    throws HTTPException
  {
    if(SSIResource.debug)
      System.out.println("@@@@ handle: "+
			 (request.isInternal() ? "internal" : "external") ) ;

    checkContent() ;
	
    Integer depth =
      (Integer) request.getState(STATE_DEPTH) ;
    if(depth == null) depth = new Integer(0) ;

    int unparsedSize = 0 ;

    Segment[] segments = getSegments() ;
    if(segments == null) {
      parseFirstTime() ;
      if( (segments = getSegments()) == null )
	return null ; // Last resort: fall back to superclass
    }
    Reply reply = null ;
    try {
      // Obtain a command registry
      updateRegistry() ;

      vars = (Dictionary)
	request.getState(STATE_VARIABLES) ;

      // Initialize the registry-dependent variables:
      vars = commReg.initVariables(this,request,vars) ;

      // Add our "very global" variables
      vars.put("secure",getValue(ATTR_SECURE,Boolean.TRUE)) ;
      vars.put("maxDepth",getValue(ATTR_MAX_DEPTH,new Integer(10))) ;
      vars.put("depth",depth) ;
      vars.put("registry",commReg) ;

      // Prepare the initial reply
      // (which represents the unparsed parts of the document)
      // and a prototype reply for segments that return null.
      reply = createDefaultReply(request,HTTP.OK) ; 
      Reply defSegReply = createDefaultReply(request,HTTP.OK) ;

      int unpSize = getUnparsedSize() ;
      if(unpSize == -1) 
	reply.setHeaderValue(Reply.H_CONTENT_LENGTH,null) ;
      else 
	reply.setContentLength(unpSize) ;
      defSegReply.setHeaderValue(Reply.H_CONTENT_LENGTH,null) ;

      long ims = request.getIfModifiedSince() ;
      long cmt = getFileStamp() ;	// used to be getLastModified()
      // should be something better
      // than either
      if(SSIResource.debug)
	System.out.println("@@@@ IMS: "+cmt+" vs "+ims) ;
      if(ims != -1 && cmt != -1 && cmt <= ims) {
	reply.setStatus(HTTP.NOT_MODIFIED) ;
	defSegReply.setStatus(HTTP.NOT_MODIFIED) ;
      } else if(ims != -1) {
	if(SSIResource.debug)
	  System.out.println("@@@@ Removed NOT MODIFIED") ;
      }

	    
      if(cmt != -1)
	defSegReply.setLastModified(cmt) ;
	    
      // For each segment:
      // 	. obtain a reply,
      // 	. merge its headers with the global reply's headers,
      Reply[] partReps = new Reply[segments.length] ;
      for(int i=0;i<segments.length;i++) {
	if(!segments[i].isUnparsed()) {
	  if(SSIResource.debug)
	    System.out.println("@@@@ Analyzing segment " +
			       segments[i]) ;

	  partReps[i] = segments[i].init(this, request, vars, commReg, i);

	  if(SSIResource.debug && partReps[i] == null)
	    System.out.println("@@@@                   " +
			       "(null segment)") ;
		    
	  merge(reply,
		partReps[i] != null ? partReps[i] : defSegReply) ;
	}
      }

      // Set a stream, unless we're not supposed to.
      // Also handle the case of no command segments.
      switch(reply.getStatus()) {
      default:
	reply.setStream
	  (new SSIStream(segments,
			 partReps,
			 new RandomAccessFile(file,"r"))) ;
      case HTTP.NO_CONTENT:
      case HTTP.NOT_MODIFIED:
      }

      if(SSIResource.debug)
	System.out.println("@@@@ Last-modified: " +
			   reply.getLastModified()) ;

      reply.setDate(System.currentTimeMillis()) ;
      return reply ;

    } catch(SSIException ex) {
      reply = createDefaultReply(request,HTTP.INTERNAL_SERVER_ERROR) ;
      reply.setContent("SSIResource is misconfigured: "+
		       ex.getMessage());
      throw new HTTPException(reply) ;
    } catch(Exception ex) {
      ex.printStackTrace() ;
      if(SSIResource.debug) {
	if(SSIResource.debug)
	  System.out.println("@@@@ Fallback to FileResource") ;
      }
      return null ;  // Last resort: fall back to superclass
    }
  }

  // The headers to merge and their corresponding callbacks
  // (more to come)
  private static final int mergeHeaders[] =
  {
    Reply.H_AGE,
    Reply.H_CONTENT_LENGTH,
    Reply.H_EXPIRES,
    Reply.H_LAST_MODIFIED,
  } ;

  private static final Merger mergers[] =
  {
    new IntMaximizer(),	// 	Reply.H_AGE
    new IntAdder(),		// 	Reply.H_CONTENT_LENGTH
    new DateMinimizer(),	// 	Reply.H_EXPIRES
    new DateMaximizer()	// 	Reply.H_LAST_MODIFIED
  } ;

  /**
   * Merges the headers (and status code) of a segment's reply with
   * those of the global reply.
   *
   * @param glob the global reply
   * @param part the segment's partial reply
   */
  private void merge(Reply glob, Reply part)
  {
    // Deal with status code first
    int pstat = part.getStatus() ;
    int gstat = glob.getStatus() ;
	
    if(pstat == HTTP.NOT_MODIFIED) {
      switch(gstat) {
      default:
	glob.setStatus(HTTP.OK) ;
      case HTTP.NOT_MODIFIED:
      }
    } else if(gstat == HTTP.NOT_MODIFIED) {
      if(SSIResource.debug)
	System.out.println("**** removed NOT MODIFIED") ;
      glob.setStatus(HTTP.OK) ;
    }
	
    // Now handle headers
    // "pointers to methods" would make this simpler
    for(int i=0;i<mergeHeaders.length;i++) 
      glob.setHeaderValue(mergeHeaders[i],
			  mergers[i]
			  .merge(glob.getHeaderValue(mergeHeaders[i]),
				 part.getHeaderValue(mergeHeaders[i]))) ;
	
    // Now handle annoying quasi-headers:
    int pint,gint ;

    // Cache-Control: max-age=n
    // (don't merge if set as attribute)
    if( getMaxAge() != -1 &&
	(pint = part.getMaxAge()) != -1 ) {
      if( (gint = glob.getMaxAge()) != -1)
	pint = Math.min(gint,pint) ;
      glob.setMaxAge(pint) ;
    }
  }


  /**
   * Retrieves the segments from the attribute
   * @return An array of segments
   */
  private final Segment[] getSegments()
  {
    return (Segment[]) getValue(ATTR_SEGMENTS,null) ;
  }
	
  /**
   * Updates the working command registry if either the registryClass
   * attribute has changed or it has never been created before.
   * <p>To avoid unnecessarily creating command registry
   * instances, this method will keep track of which kinds of command
   * registries have been created, and avoid making duplicates.
   */
  private void updateRegistry() 
    throws SSIException 
  {
    try {
      Class attrRegClass = (Class) getValue(ATTR_REGISTRY_CLASS, null) ;
      if(attrRegClass == null)
	attrRegClass = Class.forName
	  ("w3c.jigsaw.ssi.DefaultCommandRegistry") ;
	    
      if(regClass == null ||
	 !attrRegClass.equals(regClass)) {
	regClass = attrRegClass ;
	commReg = fetchRegistry(regClass) ;
      }
    } catch(ClassNotFoundException ex) {
      throw new SSIException("Cannot make registry: "+ex.getMessage()) ;
    }
  }


  /**
   * Returns an instance of the given command registry class,
   * either a new instance, or an old one if it exists in the dictionary.
   */
  private CommandRegistry fetchRegistry(Class regClass)
    throws SSIException
  {
    try {
      CommandRegistry reg = (CommandRegistry) regs.get(regClass) ;
      if(reg!=null) 
	return reg;
      else {
	reg = (CommandRegistry) regClass.newInstance();
	regs.put(regClass,reg) ;
	return reg;
      }
    } catch(Exception ex) {
      throw new SSIException("Cannot fetch command registry: "+
			     ex.getMessage()) ;
    }
  }
    

  /** Reads the unparsed file into memory, if not already done */
  private byte[] readUnparsed()
    throws IOException
  {
    File file = getFile();
    ByteArrayOutputStream out =
      new ByteArrayOutputStream((int)file.length()) ;
	
    FileInputStream in = new FileInputStream(file);

    byte[] buf = new byte[4096] ;
    int len = 0;
	
    while( (len = in.read(buf)) != -1) 
      out.write(buf,0,len);
	
    in.close();
    out.close();
	
    byte[] unparsed = out.toByteArray() ;
    return unparsed ;
  }
	

  /**
   * Does a first-time parse and sets the segment list attribute
   * accordingly.
   */
  private void parseFirstTime()
  {
    byte[] unparsed = null ;
    try {
      unparsed = readUnparsed() ;
    } catch(IOException ex) {
      setValue(ATTR_SEGMENTS,null) ;
      return ;
    }

    // The parsing code was adapted from phttpd 
	
    int byteIdx = 0, startInc, endInc, startParam, endParam, paramIdx,i ;
    byte ch, quote ;
    int max ,length = 0;
    boolean valueFound ;
	
    int unparsedSize = 0 ;
	
    // For maintaining the segment list 
    Vector buildSegments = new Vector(20) ;

    StringBuffer cmdBuf = null ;
    String cmdName = null ;
    Vector /*<String>*/ parNames = null ;
    Vector /*<String>*/ parValues = null ;
    String name = null , value = null ;
	
    // To store where the last segment ended
    int lastSegEnd = 0;

    do {
      byteIdx += 4;
      while(byteIdx < unparsed.length) {
	if( (ch = unparsed[byteIdx]) == (byte) '#' )
	  if(byteArrayNEquals(unparsed, byteIdx-4,
			      startPat, 0,
			      4)) {
	    break;
	  }

	// This is an ugly work-around to the
	// absence of unsigned bytes in Java.
	byteIdx += startIncrements[ch>=0 ? ch : 0];
      }

      if(++byteIdx >= unparsed.length)
	break; // Nothing found
	    
      // Record the start of the command name and parameter list
      startInc = (startParam = paramIdx = byteIdx) - 5 ;
	    
      // Add the previous segment of unparsed text
      // (Unless empty)
      if(startInc > lastSegEnd) {
	buildSegments.addElement(new Segment(lastSegEnd,
					     startInc));
	unparsedSize += startInc - lastSegEnd ;
	lastSegEnd = startInc ;
      }

      // Now find the end of the comment
      byteIdx += 2;
      while(byteIdx < unparsed.length) {
	if( (ch = unparsed[byteIdx]) == (byte) '>')
	  if(unparsed[byteIdx-2] == (byte) '-' &&
	     unparsed[byteIdx-1] == (byte) '-')
	    break;

	// This is an ugly work-around to the absence of
	// unsigned bytes in Java:
	byteIdx += endIncrements[ch>=0 ? ch : 0] ;
      }
      if(++byteIdx >= unparsed.length)
	break; // No end found

      // The end of the parameter list is 3 bytes earlier
      endParam = byteIdx - 3 ;
	    
      // Record the nominal end of the command segment
      endInc = byteIdx ;

      // Skip white space before command 
      while(paramIdx < endParam && isSpace(unparsed[paramIdx]) )
	paramIdx++;
      if( paramIdx >= endParam )
	continue; // No command name
	    

      max = endParam - paramIdx ;

      cmdName = parseCmdName(unparsed,paramIdx,max) ;

      // If not found, take this one as unparsed and
      // search for the next include.
      if(cmdName == null) {
	buildSegments.addElement(new Segment(startInc,
					     endInc));
	unparsedSize += endInc - startInc ;
	lastSegEnd = endInc ;
	continue;
      }

      parNames = new Vector(5) ;
      parValues = new Vector(5) ;

      parseCmdParams(unparsed,
		     paramIdx+cmdName.length(),
		     endParam,
		     parNames,parValues) ;

      buildSegments.addElement( new Segment(this,
					    cmdName,
					    new
					    ArrayDictionary(parNames,
							    parValues),
					    lastSegEnd,
					    endInc)) ;
      lastSegEnd = endInc ;
	    
    } while(byteIdx < unparsed.length) ;
	
	
    // Add the last chunk of unparsed text as a segment
    buildSegments.addElement(new Segment(lastSegEnd,
					 unparsed.length));
    unparsedSize += unparsed.length - lastSegEnd ;

    setUnparsedSize(unparsedSize) ;
	
    Segment[] segs = new Segment[buildSegments.size()] ;
    buildSegments.copyInto(segs) ;
    setValue(ATTR_SEGMENTS,segs) ;
  }

  private final String parseCmdName(byte[] unparsed,int start,int max)
  {
    StringBuffer cmdBuf = new StringBuffer(80) ;
    char ch ;
    for(int i=0;i<max;i++) {
      ch = (char) unparsed[start+i];
      if(Character.isSpace(ch)) break ;
      cmdBuf.append(ch) ;
    }
    return cmdBuf.length() == 0? null : cmdBuf.toString() ;
  }

  private final void parseCmdParams(byte[] unparsed,
				    int start,int end,
				    Vector names,Vector values)
  {
    String name = null ;
    String value = null ;
    int startParam = -1 ;

    int paramIdx = start ;
    while(paramIdx < end) {
      while(isSpace(unparsed[paramIdx++]))
	;
      if(paramIdx >= end)
	break;
	    
      byte ch = unparsed[--paramIdx];
      startParam = paramIdx;
      while(paramIdx < end
	    && !isSpace(ch)
	    && ch != (byte) '=')
	ch = unparsed[++paramIdx];
	    
      int length = paramIdx - startParam ;
      if(length<=0) break;
	    
      name = new String(unparsed,0,startParam,length) ;
      value =  emptyString ;
	    
      boolean valueFound = false ;
      while(isSpace(ch)
	    || ch == (byte) '=') {
	if(ch == (byte) '=')
	  valueFound = true ;
	ch = unparsed[++paramIdx] ;
      }
	    
      if(paramIdx >= end)
	valueFound = false ;

      byte quote ;
      if(valueFound)
	if(ch == '"' || ch == '\'' ) {
	  quote = ch ;
	  ch = unparsed[++paramIdx];
		    
	  startParam = paramIdx ;
	  while(paramIdx < end && ch != quote )
	    ch = unparsed[++paramIdx];
	  length = paramIdx - startParam ;
	  value = new String(unparsed,
			     0,
			     startParam,
			     length);
	  paramIdx++ ;  
	} else {
	  startParam = paramIdx ;
	  while(paramIdx < end
		&& ! isSpace(ch))
	    ch = unparsed[++paramIdx];
	  length = paramIdx - startParam ;
	  value = new String (unparsed,
			      0,
			      startParam,
			      length);
	}
      names.addElement(name) ;
      values.addElement(value) ;
    }
	
  }
	
  /**
   * Analogous to standard C's <code>strncmp</code>, for byte arrays.
   * (Should be in some utility package, I'll put it here for now)
   * @param ba1 the first byte array
   * @param off1 where to start in the first array
   * @param ba2 the second byte array
   * @param off2 where to start in the second array
   * @param n the length to compare up to
   * @return <strong>true</strong> if both specified parts of the arrays are
   *           equal, <strong>false</strong> if they aren't .
   */
  public static final boolean byteArrayNEquals(byte[] ba1, int off1,
					       byte[] ba2, int off2,
					       int n)
  {
    // So that only one addition is needed inside loop
    int corr = off2 - off1;
    int max = n+off1;
    for(int i=off1;i<max;i++) 
      if(ba1[i] != ba2[i+corr])
	return false;
    return true;
  }

  /**
   * Does the same as Character.isSpace, without need to cast the
   * byte into a char.
   * @param ch the character
   * @return whether or not ch is ASCII white space
   * @see java.lang.Character#isSpace
   */
  private final boolean isSpace(byte ch)
  {
    return ch==' ' || ch=='\t' || ch=='\n' || ch=='\r' ;
  }

  public long getLastModified()
  {
    long a = super.getLastModified() ;
    return a - a % 1000 ;
  }
  public final Reply createDefaultReply(Request request, int status)
  {
    Reply reply = super.createDefaultReply(request,status) ;
    reply.setHeaderValue(Reply.H_LAST_MODIFIED,null) ;
    return reply ;
  }

  public final Reply createCommandReply(Request request, int status)
  {
    return createDefaultReply(request,status) ;
  }
  
  static {
    // Initialize search increments first
    for(int i=0;i<128;i++) {
      startIncrements[i] = 5 ;
      endIncrements[i] = 3 ;
    }
    startIncrements[(int)('<')] = 4;
    startIncrements[(int)('!')] = 3;
    startIncrements[(int)('-')] = 1;
    endIncrements[(int)('-')] = 1;
      
    // Initialize attributes
    Attribute a = null;
    Class cls = null;
    Class regClass = null ;
      
    try {
      cls = Class.forName("w3c.jigsaw.ssi.SSIResource") ;
      regClass = Class.forName("w3c.jigsaw.ssi.DefaultCommandRegistry") ;
    } catch (Exception ex) {
      ex.printStackTrace();
      System.exit(0);
    }

    // The maxDepth attribute
    a = new IntegerAttribute("maxDepth"
			     , new Integer(10)
			     , Attribute.EDITABLE) ;
    ATTR_MAX_DEPTH = AttributeRegistry.registerAttribute(cls,a) ;
      
    // The secure attribute
    a = new BooleanAttribute("secure"
			     , Boolean.TRUE
			     , Attribute.EDITABLE) ;
    ATTR_SECURE = AttributeRegistry.registerAttribute(cls,a) ;
      
    // The segments attribute
    a = new SegmentArrayAttribute("segments"
				  , null
				  , Attribute.COMPUTED) ;
    ATTR_SEGMENTS = AttributeRegistry.registerAttribute(cls,a) ;
      
    // The unparsedSize attribute
    a = new IntegerAttribute("unparsedSize"
			     , null
			     , Attribute.COMPUTED) ;
    ATTR_UNPARSED_SIZE = AttributeRegistry.registerAttribute(cls,a) ;
      
    // The registryClass attribute
    a = new ClassAttribute("registryClass"
			   , regClass
			   , Attribute.EDITABLE ) ;
    ATTR_REGISTRY_CLASS = AttributeRegistry.registerAttribute(cls,a) ;
      
  }

}

/**
 * Merger classes are used to provide callbacks and make
 * header merging more uniform. (Though it may be overkill...)
 */
abstract class Merger {
    abstract HeaderValue merge(HeaderValue g,HeaderValue p) ;
}

class IntMaximizer extends Merger {
    HeaderValue merge(HeaderValue g,HeaderValue p)
    {
	if(p != null) {
	    if(g != null) {
		((HttpInteger) g)
		    .setValue(Math.max( ((Integer) g.getValue()).intValue() ,
					((Integer) p.getValue()).intValue()  )) ;
	    } else return p ;
	}
	return g ;
    }
}	    

class IntMinimizer extends Merger {
    HeaderValue merge(HeaderValue g,HeaderValue p)
    {
	if(p != null) {
	    if(g != null) {
		((HttpInteger) g)
		    .setValue(Math.min(((Integer) g.getValue()).intValue() ,
					((Integer) p.getValue()).intValue())) ;
	    } else return p ;
	}
	return g ;
    }
}

class IntAdder extends Merger {
    HeaderValue merge(HeaderValue g,HeaderValue p) {
	if(SSIResource.debug)
	    System.out.println("&&&& Adder: g="+g+", p="+p) ;
	if(g != null) {
	    if(p != null) {
		int b = ((Integer) g.getValue()).intValue() +
			       ((Integer) p.getValue()).intValue() ;

		((HttpInteger) g)
		    .setValue( b  ) ;
	    } else return null ;
	}
	return g ;
    }
}

class DateMinimizer extends Merger {
    HeaderValue merge(HeaderValue g,HeaderValue p)
    {
	if(p != null) {
	    if(g != null) {
		((HttpDate) g)
		    .setValue(Math.min(((Long) g.getValue()).longValue() ,
				       ((Long) p.getValue()).longValue())) ;
	    } else return p ;
	}
	return g ;
    }
}
class DateMaximizer extends Merger {
    HeaderValue merge(HeaderValue g,HeaderValue p)
    {
	if(p != null) {
	    if(g != null) {
	        ((HttpDate) g)
		    .setValue(Math.max(  ((Long) g.getValue()).longValue() ,
					 ((Long) p.getValue()).longValue())) ;
	    } else return p ;
	}
	return g ;
    }
}



	
