package org.dbxml.core.request;

/*
 * dbXML License, Version 1.0
 *
 *
 * Copyright (c) 1999-2001 The dbXML Group, L.L.C.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by
 *        The dbXML Group (http://www.dbxml.com/)."
 *    Alternately, this acknowledgment may appear in the software
 *    itself, if and wherever such third-party acknowledgments normally
 *    appear.
 *
 * 4. The names "dbXML" and "The dbXML Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    info@dbxml.com.
 *
 * 5. Products derived from this software may not be called "dbXML",
 *    nor may "dbXML" appear in their name, without prior written
 *    permission of The dbXML Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE DBXML GROUP OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: URIMapper.java,v 1.36 2001/08/05 07:23:00 tom Exp $
 */

import org.dbxml.server.*;
import org.dbxml.core.*;
import org.dbxml.core.Collection;
import org.dbxml.xml.*;
import org.dbxml.core.objects.*;

import org.w3c.dom.*;

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

/**
 * URIMapper maps a URI (from whence it came) to a dbXML object.  dbXML
 * URIs can identify any of several different object types, each of which
 * exposes a different (or slightly different) interface to the outside
 * world.
 */

public final class URIMapper extends URLConnection implements Poolable {

   public static final int UNKNOWN = -1;
   public static final int APPLICATION = 1;
   public static final int COLLECTION = 2;
   public static final int DOCUMENT = 3;
   public static final int XMLOBJECT = 4;

   private static final String[] EmptyStrings = new String[0];

   private ObjectPool   pool = null;
   private String       uri = null;
   private int          type = 0;

   private byte[]       buf = null;
   private int          pos = 0;
   private char         lastChar = 0;

   private Database  db = null;
   private Collection   collection = null;
   private Document     document = null;
   private XMLObject    xmlObject = null;
   private String       method = null;
   private Container    container = null;

   private Properties   params = null;
   private String[]     args = null;

   private String       urlresult = null;       // Holds the value of the URL resolution results (XML doc or XMLObject call results )
   private boolean      inputstreamset = false; // Flag to tell if the input stream has been initialized

   /** Constructor for creating URIMapper instance using a standard URL
    */
   public URIMapper(URL u) {
      super(u);
      try {
         setURI(u.toString());
      }
      catch ( Exception e) {
         // Put error code here!
         org.dbxml.Debug.printStackTrace(e);
      }
   }

   /** Constructor for older URIMapper instances
    */
   public URIMapper(String uri) throws dbXMLException {
      super(null);
      setURI(uri);
   }

   /** Constructor for older URIMapper instances
    */
   public URIMapper() {
      super(null);
   }

   /* Code for new URL instance of URIMapper */
   public Object invokeRequest() throws XMLObjectException {

      Reflectable obj;
      obj = (Reflectable)getXMLObject();

      String name = getMethod();
      Reflector ref = obj.getReflector();
      Object result;

      Properties props = getProperties();
      if ( props != null )
         result = ref.invoke(obj, name, new MapArgs(props));
      else
         result = ref.invoke(obj, name, getArguments());

      return result;
   }

   /**
      Opens a communications link to the resource referenced by this URL,
      if such a connection has not already been established.
   */
   public void connect() {
      this.connected=true;
   }

   /**
      Returns an input stream that reads from this open connection.
   */
   public InputStream getInputStream() {

      String output = null ;
      Object result = null ;

      // Check if the inputstream has already been initialized, if not do now, else return the contents of urlresult
      if ( ! inputstreamset ) {
         try {
            switch ( type ) {

               case URIMapper.DOCUMENT:
                  Writer doc = new StringWriter();
                  result = this.getDocument();
                  TextWriter.write((Node)result, doc);
                  output = doc.toString();
                  break;

               case URIMapper.XMLOBJECT:
                  result = invokeRequest();

                  if ( result instanceof Node ) {
                     Writer ser = new StringWriter();
                     TextWriter.write((Node)result, ser);
                     output = ser.toString();
                  }
                  else
                     if ( result != null ) {
                        output = result.toString();
                     }
                  break;

               default:
                  //  Document type not found, output error message
                  throw new Exception("Content type unsupported");
            }
         }
         catch ( Exception e ) {
            org.dbxml.Debug.printStackTrace(e);
         }
      }
      // Inputstream already initialized, set output to be value of urlresult
      else {
         output=urlresult ;
      }

      // Save the value of output into local variable urlresult
      urlresult = output ;
      // Set inpustreamset flag to signal that url has already been resolved
      inputstreamset = true ;

      // Send the result to the client, sending blank string if NULL
      if ( output == null)
         return new StringBufferInputStream("");
      else
         return new StringBufferInputStream(output);

   }


   /**
    * Returns the value of the content-encoding header field.
    * Returns:
    *       the content encoding of the resource that the URL references, or null if not known.
    */
   public String getContentEncoding() {
      // For now this method only returns null values since we are not currenlty encoding documents
      return null;
   }


   /**
    * Returns the value of the content-type header field.
    * Returns:
    *       the content type of the resource that the URL references, or null if not known.
    */
   public String getContentType() {
      // Return the docuement's content type, for now this can only be "text/xml"
      return new String("text/xml");
   }

   /**
    * Returns the value of the content-length header field.
    * Returns:
    *       the content length of the resource that this connection's URL references, or -1 if the content length is not known.
    */
   public int getContentLength() {
      // Check to see if this document has already been resolved, if not call getInputStream
      if ( ! inputstreamset ) {
         this.getInputStream();
      }

      // Return the lenght of this document/XMLObject call
      return urlresult.length();
   }

   /**
    * Returns the value of the last-modified header field. The result is the number of milliseconds since January 1, 1970 GMT.
    * Returns:
    *  the date the resource referenced by this URLConnection was last modified, or 0 if not known.
    */
   public long getLastModified() {
      // For now this functionality is not available, return 0
      return 0 ;
   }





   public void setPool(ObjectPool pool) {
      this.pool = pool;
   }

   public void reclaim() {
      if ( xmlObject != null && xmlObject instanceof Poolable )
         ((Poolable)xmlObject).reclaim();
      reset();
      if ( pool != null )
         pool.putObject(this);
   }

   /**
    * reset resets the state of the URIMapper.
    */
   private void reset() {
      type = UNKNOWN;
      lastChar = 0;
      method = "";
      db = null;
      collection = null;
      document = null;
      xmlObject = null;
      params = null;
      args = null;
      container = null;

      // reset urlresult and inputstreamset
      urlresult = null ;
      inputstreamset = false ;
   }




   /**
    * setURI sets the URI for the URIMapper and parses it.  The parsed
    * components of a URI can be retrieved using getObjectType and any
    * of the get<component> methods.
    *
    * @param uri The URI
    */
   public void setURI(String uri) throws dbXMLException {
      this.uri = uri;
      reset();
      parse();
   }

   /**
    * parseName parses an identifier up to any of the specified delimiters.
    *
    * @param delims The delimiters to use
    * @return The parsed name
    */
   private String parseName(String delims) {
      int start = pos;
      while ( pos < buf.length ) {
         lastChar = (char)buf[pos++];
         if ( delims.indexOf(lastChar) != -1 )
            break;
      }
      if ( pos == buf.length && delims.indexOf(lastChar) == -1 )
         pos++;
      return pos > start ? new String(buf, start, pos-start-1)
                         : "";
   }

   /**
    * parseParams parses a parameter set and produces either a Properties or
    * String[] Object representing those parameters.
    */
   private void parseParams() {
      if ( lastChar == '?' ) {
         // Query String param list
         params = new Properties();
         String name;
         String value;
         String temp;
         while ( true ) {
            name = parseName("=");
            if ( name.length() == 0 )
               break;
            value = parseName("?&;");
            temp = params.getProperty(name);
            if( temp != null ) {
               StringBuffer sb = new StringBuffer(32);
               sb.append(temp);
               sb.append('\u0001');
               sb.append(value);
               value = sb.toString();
            }
            params.setProperty(name, value);
         }
      }
      else if ( lastChar == '(' ) {
         // Java-like argument list
         // TODO: This code is nowhere near perfect... It could use a good gutting
         String tmp = new String(buf, pos, buf.length-pos);
         StringReader bis = new StringReader(tmp);
         StreamTokenizer st = new StreamTokenizer(bis);
         st.resetSyntax();
         st.whitespaceChars(0, 32);
         st.wordChars(48, 122);
         st.wordChars(13, 13);
         st.ordinaryChars(33, 46);
         st.ordinaryChars(58, 64);
         st.ordinaryChars(91, 96);
         st.ordinaryChars(123, 127);
         st.slashSlashComments(false);
         st.slashStarComments(false);
         st.eolIsSignificant(false);
         st.quoteChar('\"');
         st.quoteChar('\'');
         st.quoteChar('|');
         st.commentChar('/');
         List list = new ArrayList();
         boolean done = false;
         try {
            while ( !done && st.nextToken() != StreamTokenizer.TT_EOF ) {
               switch ( st.ttype ) {

                  case StreamTokenizer.TT_WORD:
                     list.add(st.sval);
                     break;

                  case '\"':
                     list.add(st.sval);
                     break;

                  case '\'':
                  case '|':
                     list.add(st.sval);
                     break;

                  case '[':
                     boolean doneArray = false;
                     StringBuffer sb = new StringBuffer();
                     while ( !doneArray && st.nextToken() != StreamTokenizer.TT_EOF) {
                        switch ( st.ttype ) {

                           case StreamTokenizer.TT_WORD:
                              if ( sb.length() > 0 )
                                 sb.append('\u0001');
                              sb.append(st.sval);
                              break;

                           case ']':
                              doneArray = true;
                              break;

                        }
                     }
                     list.add(sb.toString());
                     break;

                  case ')':
                     done = true;
                     break;

               }
            }
         }
         catch ( Exception e ) {
            org.dbxml.Debug.printStackTrace(e);
            return;
         }
         args = (String[])list.toArray(EmptyStrings);
      }
      else
         params = new Properties();
   }

   private void parseXMLObject() {
      try {
         if ( lastChar == '/' )
            method = parseName("/(?");
         if ( method.length() == 0 )
            method = "execute";

         type = XMLOBJECT;
         parseParams();
      }
      catch ( Exception e ) {
         org.dbxml.Debug.printStackTrace(e);
      }
   }

   /**
    * parse parses the URI.
    */
   private void parse() throws dbXMLException {
      buf = uri.getBytes();
      pos = 0;
      String tmp;

      if ( buf.length == 0 ) {
         throw new DBException(FaultCodes.URI_EMPTY);
      }
      
      // TODO: Be Able To Handle Remote URIs
      if ( (char)buf[0] != '/' ) {
         parseName(":"); // Ignore Protocol
         parseName("/"); // Ignore Slash
         parseName("/"); // Ignore Slash
         parseName("/:"); // Ignore Host (For Now)
         if ( lastChar == ':' )
            parseName("/"); // Ignore Port
      }
      else
         pos = 1;

      // Database check
      tmp = parseName("/");
      if ( tmp == null )
         return;

      db = Database.getDatabase(tmp);
      if ( db == null )
         return;

      type = APPLICATION;
      tmp = parseName("/(?");


      int objType = getParsedObjectType(db, tmp);
      // If unknown then this URI just points to db and we're done.
      // Otherwise we need to keep walking down the URI.
      if (objType != UNKNOWN) {
         type = walkURI(db, tmp, objType);
      }

      if ( lastChar == '?' ) {
         parseParams();
         return;
      }
   }

   /**
    * Recursive method to handle the parse of the URI and setup the instance
    * objects.
    */
   protected int walkURI(Collection col, String name, int objType)
         throws dbXMLException {
      switch (objType) {
         case DOCUMENT:
            container = col.getContainer(name);
            document = container.getDocument();
            return DOCUMENT;

         case XMLOBJECT:
            xmlObject = col.getXMLObject(name);
            if ( xmlObject != null ) {
               parseXMLObject();
               return XMLOBJECT;
            }
            break;

         case COLLECTION:
            Collection c = col.getCollection(name);
            if ( c != null ) {
               collection = c;
               String tmp = parseName("/(?");
               // If we have another name recurse to handle it.
               if (! tmp.equals("")) {
                  return walkURI(c, tmp, getParsedObjectType(c, tmp));
               }

               return COLLECTION;
            }
      }

      return UNKNOWN;
   }

   /**
    * Determine the type of object. If more then one object has the same name
    * the order of precedence is COLLECTION - XMLOBJECT - DOCUMENT
    */
   protected int getParsedObjectType(Collection col, String name)
         throws dbXMLException {

      if (col.getCollection(name) != null) {
         return COLLECTION;
      }
      else if (col.getXMLObject(name) != null) {
         return XMLOBJECT;
      }
      else if ((col.getFiler() != null) && (col.getContainer(name) != null)) {
         return DOCUMENT;
      }
      else {
         return UNKNOWN;
      }
   }

   /**
    * getObjectType returns the type of Object that was identified in the
    * parsing of the URI.  This method will return one of the following
    * values: UNKNOWN, APPLICATION, DATABASE, COLLECTION, DOCUMENT or
    * XMLOBJECT.
    *
    * @return The object type
    */
   public int getObjectType() {
      return type;
   }

   /**
    * getDatabase returns the Database that was resolved in the
    * parsing of this URI.
    *
    * @return The Database
    */
   public Database getDatabase() {
      return db;
   }

   /**
    * getCollection returns the Collection object that was resolved in the
    * parsing of the URI.  If no Collection was resolved, this method will
    * return null.
    *
    * @return The Collection
    */
   public Collection getCollection() {
      return collection;
   }

   /**
    * getDocument returns the Document object that was resolved in the
    * parsing of the URI.  If no Document was resolved, this method will
    * return null.
    *
    * @return The Document
    */
   public Document getDocument() {
      return document;
   }

   /**
    * getContainer returns the Document Container that was resolved in
    * the parsing of the URI.  If no Container was resolved, this method
    * will return null.
    *
    * @return The Container
    */
   public Container getContainer() {
      return container;
   }

   /**
    * getXMLObject returns the XMLObject that was resolved in the parsing
    * of the URI.  If no XMLObject was resolve, this method will return
    * null.
    *
    * @return The XMLObject
    */
   public XMLObject getXMLObject() {
      return xmlObject;
   }

   /**
    * getMethod returns the method name that was resolved in the parsing
    * of the URI.  Method names are associated with XMLObjects.
    * If no method name was resolved, this method will return null.
    *
    * @return The method name
    */
   public String getMethod() {
      return method;
   }

   /**
    * getProperties returns the Properties object that was produced in
    * parsing the URI's Query String.  Properties are passed into methods
    * that are associated with XMLObjects.  This method will return null if
    * no Properties were resolved.
    *
    * @return The Query String Properties
    */
   public Properties getProperties() {
      return params;
   }

   /**
    * getArguments returns method arguments in the form of a String array.
    * Method arguments are passed to a method in the order that they are
    * specified in the URI.  If no arguments were parsed, this method will
    * return null.
    *
    * @return The method arguments
    */
   public String[] getArguments() {
      return args;
   }
}


