/*
  Copyright (C) 2006 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/
package org.opengroupware.jope.appserver.publisher;

import java.util.Arrays;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.appserver.core.WOContext;
import org.opengroupware.jope.foundation.NSDisposable;
import org.opengroupware.jope.foundation.NSObject;
import org.opengroupware.jope.foundation.UString;

/*
 * JoTraversalPath
 * 
 * This represents a Jo path lookup. It contains the operation to perform the
 * object traversal and it keeps the state associated with it.
 * 
 * Note:
 * This takes an array of names because splitting a URL into name parts involves
 * appropriate escaping! A simple split is not enough since the names could
 * contain escape sequences representing the splitchar.
 * Summary: resist adding a covenience constructor which takes a path in
 * String representation ;-)
 * 
 * THREAD:
 * This object is not threadsafe. Its not recommended to be used in multiple
 * threads and its not recommended to store the object in a persistent way.
 */
public class JoTraversalPath extends NSObject implements NSDisposable {
  protected static final Log log = LogFactory.getLog("JoTraversalPath");

  /* configuration */
  protected String[]  path;
  protected Object    rootObject;
  protected WOContext context;
  protected boolean   doAquire;
  protected boolean   ignoreLastName; /* use for MKCOL, last name is absent */
  protected boolean   ignoreMissingLastName; /* for PUT on new resources */
  
  /* traversal results */
  protected Object[]  objects;
  protected String[]  pathInfo;
  protected Exception lastException;
  protected Object    clientObject;
  protected Object    object;

  /* transient iteration state */
  protected Object    contextObject;
  protected String    lookupName;
  protected int       traversalIndex;
  protected boolean   isLastName;

  public JoTraversalPath(String[] _path, Object _root, WOContext _ctx) {
    this.path           = _path;
    this.context        = _ctx;
    this.rootObject     = _root;
    this.doAquire       = false;
    this.ignoreLastName = false;
    this.ignoreMissingLastName = false;
  }
  
  /* accessors */
  
  public WOContext context() {
    return this.context;
  }
  
  /* results */
  
  public Object resultObject() {
    return this.object;
  }
  public Object clientObject() {
    return this.object;
  }
  
  public Exception lastException() {
    return this.lastException;
  }
  
  public String[] pathInfo() {
    return this.pathInfo;
  }
  
  /* operation */
  
  public void reset() {
    // TODO: we might want to call sleep or something like this
    this.objects        = null;
    this.lastException  = null;
    this.traversalIndex = 0;
    this.contextObject  = null;
    this.lookupName     = null;
    this.pathInfo       = null;
    this.isLastName     = false;
    this.clientObject   = null;
    this.object         = null;
  }
  
  public void traverse() {
    boolean debugOn = log.isDebugEnabled();
    
    this.reset();
    
    /* start at the root object */
    
    this.contextObject = this.rootObject;
    
    /* setup results array */
    
    this.objects = new Object[this.path.length];
    
    /* iterate over the path */
    
    for (this.traversalIndex = 0;
         this.traversalIndex < this.path.length;
         this.traversalIndex++)
    {
      /* setup context */
      
      this.isLastName = (this.traversalIndex + 1) == this.path.length;
      this.lookupName = this.path[this.traversalIndex];
      
      /* Special support for pathes where the last component does not yet exist,
       * eg in a MKCOL WebDAV request.
       */
      if (this.ignoreLastName && this.isLastName) {
        this.pathInfo = new String[] { this.lookupName };
        if (debugOn)
          log.debug("ignoring last path part in lookup: " + this.pathInfo);
        break; /* we are done */
      }
      
      /* perform name lookup */ 
      
      if (debugOn) { 
        log.debug("will lookup[" + this.traversalIndex + "]: " + 
                  this.lookupName);
      }
      
      Object nextObject = this.traverseKey(this.contextObject, this.lookupName);
      this.objects[this.traversalIndex] = nextObject;
      
      if (debugOn)
        log.debug("  did lookup: " + nextObject);
      
      /* process lookup results */
      
      if (nextObject == null) { /* this also catches 404 result exceptions */
        if (this.isLastName && this.ignoreMissingLastName) {
          /* Eg in PUT the last part of the path may be missing if its a new
           * resource we are creating.
           */
          this.pathInfo = new String[] { this.lookupName };
          if (debugOn) {
            log.debug("ignoring last path part in failed lookup: " +
                      this.pathInfo);
          }
          break; /* we are done */
        }
      }

      if (nextObject == null) {
        /* Check whether the current object is a JoCallable.
         * 
         * This one is tricky. We cannot break on the first JoCallable, 
         * otherwise we could not call methods on methods! Which is relevant,
         * consider a management interface editing a method, eg a URL like
         * "/myscript.py/manage".
         * So instead, we only stop if there is no 'next object'.
         */
        if (nextObject == null && 
            this.contextObject instanceof JoCallable &&
            ((JoCallable)this.contextObject).isCallableInContext(this.context))
        {
          this.pathInfo = new String[this.path.length - this.traversalIndex];
          System.arraycopy(this.path, this.traversalIndex,
                           this.pathInfo, 0,
                           this.path.length - this.traversalIndex);
          if (debugOn) {
            log.debug("  found callable, pathinfo: " + 
                      Arrays.asList(this.pathInfo));
          }
        }
        else if (debugOn) {
          log.debug("got no result and current object is not callable: " +
                    this.contextObject);
        }
        
        /* leave loop, we encountered a null object */
        break;
      }
      
      /* continue lookup at the found object */
      this.contextObject = nextObject;
    }
    
    /* define results */
    
    this.object = this.contextObject;
    if (this.object instanceof JoCallable &&
        ((JoCallable)this.object).isCallableInContext(this.context))
    {
      int lastIdx = this.traversalIndex >= this.objects.length
        ? (this.objects.length - 2)
        : (this.traversalIndex - 1);
      
      if (lastIdx < 0) {
        log.error("unexpected traversal index when calculating clientObject: " +
                  lastIdx);
      }
    }
    else
      this.clientObject = this.object;
  }
  
  public Object traverseKey(Object _cursor, String _name) {
    boolean debugOn = log.isDebugEnabled();
    
    if (debugOn) log.debug("traverse key '" + _name + "' on: " + _cursor);
    
    /* ensure that the user is allowed to access the key */
    
    Exception error = JoSecuredObject.Utility
      .validateNameOfObject(_cursor, _name, this.context);
    if (error != null) {
      if (debugOn)
        log.debug("  access to name '" + _name + "' is denied: " + error);
      this.lastException = error;
      return null;
    }
    
    /* lookup object for name */
    
    Object result = JoObject.Utility.lookupName
      (_cursor, _name, this.context, false /* do not acquire yourself */);
    
    /* process result object */
    
    if (result != null) {
      /* Catch exceptions. They cannot be returned as results and are not valid
       * inside lookup hierarchies.
       */
      if (result instanceof Exception) {
        // TODO: do we need special handling for 404 exceptions?
        this.lastException = (Exception)result;
        return null;
      }
      
      /* validate access to object if one was found */
      
      error = JoSecuredObject.Utility
        .validateValueForNameOfObject(_cursor, _name, result, this.context);
      
      if (error != null) {
        if (debugOn) {
          log.debug("  access to value for name '" + _name + "' was denied: " +
                    error);
        }
        this.lastException = error;
        return null;
      }
      
      /* lookup was successful, return value */
      return result;
    }
    
    /* no object was found, perform aquisition */
    
    if (!this.doAquire) {
      if (debugOn) {
        log.debug("lookup of name '" + _name + "' returned no result and " +
                  "aquisition is disabled.");
      }
      return null;
    }
    
    /* perform aquisition, go through previous objects and try to aquire */
    
    for (int j = this.traversalIndex - 1; j >= 0; j++) {
      error = JoSecuredObject.Utility
        .validateNameOfObject(this.objects[j], _name, this.context);
      if (error != null) {
        if (debugOn)
          log.debug("  aq access to name '" + _name + "' is denied: " + error);
        this.lastException = error;
        return null;
      }
      
      result = JoObject.Utility.lookupName
        (this.objects[j], _name, this.context, false /* do not acquire */);
      if (result != null) {
        /* aquisition succeeded */

        error = JoSecuredObject.Utility
          .validateValueForNameOfObject(_cursor, _name, result, this.context);
      
        if (error != null) {
          if (debugOn) {
            log.debug("  aq access to value for name '" + _name + 
                      "' was denied: " + error);
          }
          this.lastException = error;
          return null;
        }
      
        /* lookup was successful, return value */
        return result;
      }
    }
    
    if (debugOn) {
      log.debug("lookup of name '" + _name + "' returned no result and " +
                "aquisition of the name failed.");
    }
    return null;
  }
  
  /* dispose */
  
  public void dispose() {
    this.reset();
    
    this.path       = null;
    this.rootObject = null;
    this.context    = null;
  }

  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.path != null) {
      _d.append(" path[");
      _d.append(this.path.length);
      _d.append("]: ");
      _d.append(UString.componentsJoinedByString(this.path, "/"));
    }
    else
      _d.append(" no-path");
    
    if (this.rootObject != null)
      _d.append(" root=" + this.rootObject);
    else
      _d.append(" no-root");
    
    /* options */
    if (this.doAquire)              _d.append(" acquire");
    if (this.ignoreLastName)        _d.append(" ig-lastname");
    if (this.ignoreMissingLastName) _d.append(" ig-miss-lastname");
  }
}
