package org.dbxml.core.objects;

/*
 * 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: Reflector.java,v 1.35 2001/06/15 18:12:01 tom Exp $
 */

import org.dbxml.server.*;
import org.dbxml.core.*;
import org.dbxml.xml.dom.*;
import org.w3c.dom.*;
import java.lang.reflect.*;
import java.util.*;

/**
 * Reflector is an Object reflection and method invocation utility for dbXML
 * Reflectable implementations (XMLObjects).
 */

public final class Reflector {
   private static final String[] EmptyStrings = new String[0];

   private static final String PARAMS_PREFIX = "PARAMS_";
   private static final String DEFAULTS_PREFIX = "DEFAULTS_";
   private static final String ERRORS_PREFIX = "ERRORS_";

   private Map methods = new HashMap(); // MethodInfo

   public Reflector() {
   }

   public Reflector(Class c) throws XMLObjectException {
      reflect(c);
   }

   /**
    * reflect performs an initial reflection against the Class that this
    * Reflector will support.  This method will not be capable of reflecting
    * parameter names because the Java compiler throws them away.  In order to
    * create parameter name mappings, you must call addMethod.
    */
   public void reflect(Class c) throws XMLObjectException {
      // Short-curcuit on this class
      if ( c == SimpleXMLObject.class )
         return;

      // Only add queryInterface on this class
      if ( c == SimpleReflectable.class ) {
         try {
            String qi = "queryInterface";
            Method m = c.getMethod(qi, new Class[0]);
            MethodInfo mi = new MethodInfo(Types.VOID, qi, m, new ParamInfo[0]);
            methods.put(qi, mi);
         }
         catch ( Exception e ) {
            org.dbxml.Debug.printStackTrace(e);
         }
         return;
      }

      Method[] m = c.getDeclaredMethods();
      Set defined = new HashSet();
      for ( int i = 0; i < m.length; i++ ) {
         String name = m[i].getName();

         // Check Modifiers
         int mod = m[i].getModifiers();
         if ( !Modifier.isPublic(mod) || Modifier.isStatic(mod) )
            continue;

         // Check Valid Return Types
         Class ret = m[i].getReturnType();
         int retType = Types.typeOf(ret);
         if ( retType == Types.UNKNOWN || ret.isArray() )
            continue;

         // Ignore Special Methods from the various Context interfaces
         // This has to be the ugliest piece of shit I've ever written
         if ( ( (     name.startsWith("get")
                   || name.startsWith("set") )
                &&
                (     name.equals("getReflector")
                   || name.equals("getName")
                   || name.equals("setDatabase")
                   || name.equals("getDatabase")
                   || name.equals("setDatabase")
                   || name.equals("getDatabase")
                   || name.equals("setCollection")
                   || name.equals("getCollection")
                   || name.equals("setContainer")
                   || name.equals("getContainer")
                   || name.equals("setDocument")
                   || name.equals("getDocument")
                   || name.equals("setConfig")
                   || name.equals("getConfig")
                   || name.equals("setPool") )
              )
              || name.equals("reclaim")
                 // Java Stuff
              || name.equals("equals")
              || name.equals("finalize")
              || name.equals("hashCode")
              || name.equals("notify")
              || name.equals("notifyAll")
              || name.equals("wait") )
            continue;

         if ( defined.contains(name) )
            throw new ClassFormatException("Duplicate Exposed Method '"+name+"'");

         // Check Valid Parameter Types
         Class[] p = m[i].getParameterTypes();
         ParamInfo[] params = new ParamInfo[p.length];

         String[] names = getFieldVals(c, p.length, PARAMS_PREFIX+name);
         String[] defaults = getFieldVals(c, p.length, DEFAULTS_PREFIX+name);
         String[] errors = getFieldVals(c, p.length, ERRORS_PREFIX+name);

         boolean good = true;
         for ( int j = 0; j < p.length && good; j++ ) {
            String pname = names != null ? names[j]
                                         : "arg"+(j+1);

            int parmType = Types.typeOf(p[j]);
            good = parmType != Types.UNKNOWN;

            if ( p[j].isArray() )
               params[j] = new ParamInfo(pname, parmType, p[j].getComponentType());
            else
               params[j] = new ParamInfo(pname, parmType);

            if ( defaults != null )
               params[j].defValue = defaults[j];

            if ( errors != null )
               params[j].errValue = errors[j];
         }
         if ( !good )
            continue;

         methods.put(name, new MethodInfo(retType, name, m[i], params));

         defined.add(name);
      }
   }

   private String[] getFieldVals(Class c, int len, String name) {
      try {
         Field f = c.getDeclaredField(name);
         int mod = f.getModifiers();
         if ( Modifier.isStatic(mod) && Modifier.isFinal(mod)
           && f.getType().isArray() && f.getType().getComponentType() == String.class ) {
            String[] res = (String[])f.get(null);
            if ( res.length == len )
               return res;
         }
      }
      catch ( Exception e ) {
      }
      return null;
   }

   /**
    * addMethod adds method meta data to the reflected Class definition.
    * The params that are passed in do not absolutely need to match the
    * parameter names that are in the source code, but because they establish a
    * contract between a set of named parameters and the parameter ordering for
    * the method, it is your safest bet to match mapping parameters to source
    * code parameters.
    * <br>
    * A method without parameter name mappings will not be accessible for
    * invocation.
    *
    * @param name The method name
    * @param params The parameter names
    * @param defaults Default values for the parameters
    * @param errors Errors values on conversion failure
    * @throws XMLObjectException If a mapping error occurs
    */
   public void addMethod(String name, String[] params, String[] defaults, String[] errors) throws XMLObjectException {
      MethodInfo info = (MethodInfo)methods.get(name);
      if ( info != null ) {
         if ( info.params.length == params.length )
            for ( int i = 0; i < params.length; i++ ) {
               info.params[i].name = params[i];
               if ( defaults != null )
                  info.params[i].defValue = defaults[i];
               if ( errors != null )
                  info.params[i].errValue = errors[i];
            }
         else
            throw new ClassFormatException("Param Count For '"+name+"' Does Not Match Reflector");
      }
      else
         throw new MethodNotFoundException("Method '"+name+"' Not Found");
   }

   public void addMethod(String name, String[] params) throws XMLObjectException {
      addMethod(name, params, null, null);
   }

   public void addMethod(String name, String[] params, String[] defaults) throws XMLObjectException {
      addMethod(name, params, defaults, null);
   }

   /**
    * getMethods returns the names of the publicly accessible methods for the
    * reflected class.
    *
    * @return The method names
    */
   public String[] listMethods() {
      return (String[])methods.keySet().toArray(EmptyStrings);
   }

   /**
    * getMethodParams returns the names of the parameters that a method would
    * require to be invoked.  These names will be returned in the order by
    * which they're specified upon invocation.
    *
    * @param name The method name
    * @return The parameter names
    */
   public String[] listMethodParams(String name) {
      MethodInfo info = (MethodInfo)methods.get(name);
      String[] params = null;
      if ( info != null ) {
         params = new String[info.params.length];
         for ( int i = 0; i < params.length; i++ ) {
            params[i] = info.params[i].name;
            if ( params[i] == null )
               return null;
         }
      }
      return params;
   }

   /**
    * getReturnType returns the return type for the specified method..
    *
    * @see org.dbxml.core.objects.Types
    *
    * @param method The Method name
    * @return The Result Class
    */
   public int getReturnType(String method) {
      MethodInfo info = (MethodInfo)methods.get(method);
      if ( info != null )
         return info.result;
      return Types.UNKNOWN;
   }

   /**
    * getParamType returns the parameter type for the specified parameter in
    * the specified method.
    *
    * @see org.dbxml.core.objects.Types
    *
    * @param method The Method name
    * @param param The Parameter name
    * @return The Parameter Class
    */
   public int getParamType(String method, String param) {
      MethodInfo info = (MethodInfo)methods.get(method);
      if ( info != null ) {
         for ( int i = 0; i < info.params.length; i++ )
            if ( info.params[i].name.equals(param) )
               return info.params[i].type;
      }
      return Types.UNKNOWN;
   }

   /**
    * getParamDefault returns the default value (String) for the specified
    * parameter in the specified method.
    *
    * @param method The Method name
    * @param param The Parameter name
    * @return The default value
    */
   public String getParamDefault(String method, String param) {
      MethodInfo info = (MethodInfo)methods.get(method);
      if ( info != null ) {
         for ( int i = 0; i < info.params.length; i++ )
            if ( info.params[i].name.equals(param) )
               return info.params[i].defValue;
      }
      return null;
   }

   /**
    * isParamArray returns whether or not the parameter type for the specified
    * parameter is an array type..
    *
    * @param method The Method name
    * @param param The Parameter name
    * @return Whether or not the parameter is an array
    */
   public boolean isParamArray(String method, String param) {
      MethodInfo info = (MethodInfo)methods.get(method);
      if ( info != null ) {
         for ( int i = 0; i < info.params.length; i++ )
            if ( info.params[i].name.equals(param) )
               return info.params[i].array;
      }
      return false;
   }

   // Invocation Methods

   /**
    * invoke invokes the named method against the specified Reflectable
    * Object, passing to the method the set of specified parameters.
    *
    * @param obj The target Reflectable
    * @param name The method name
    * @param params The parameter values
    * @return The result of the method
    * @throws XMLObjectException if an invocation error occurs
    */
   public Object invoke(Reflectable obj, String name, Object[] params) throws XMLObjectException {
      MethodInfo info = (MethodInfo)methods.get(name);
      if ( info != null ) {
         try {
            return info.method.invoke(obj, params);
         }
         catch ( Exception e ) {
            throw new XMLObjectRuntimeException("Error Invoking Method '"+name+"'");
         }
      }
      else
         throw new MethodNotFoundException("Method '"+name+"' Not Found");
   }

   private boolean isTrue(String value) {
      return "[true][yes][1][y][on]".indexOf("["+value.toLowerCase()+"]") != -1;
   }

   /**
    * invoke invokes the named method against the specified Reflectable Object,
    * passing to the method the set of specified parameters.  This method will
    * convert the String array in args to the appropriate Object types before
    * invoking the method.
    *
    * @param obj The target Reflectable
    * @param name The method name
    * @param args The parameter values
    * @return The result of the method
    * @throws XMLObjectException if an invocation error occurs
    */
   public Object invoke(Reflectable obj, String name, Variant[] args) throws XMLObjectException {
      MethodInfo info = (MethodInfo)methods.get(name);
      if ( info != null ) {
         Object[] params = new Object[info.params.length];
         for ( int i = 0; i < info.params.length; i++ ) {
            ParamInfo p = info.params[i];
            if ( args[i] == null )
               args[i] = new Variant(p.defValue);
            if ( !p.array ) {
               switch ( p.type ) {

                  case Types.STRING:
                     params[i] = args[i].getString();
                     break;

                  case Types.LONG:
                     params[i] = new Long(args[i].getLong());
                     break;

                  case Types.INT:
                     params[i] = new Integer(args[i].getInt());
                     break;

                  case Types.SHORT:
                     params[i] = new Short(args[i].getShort());
                     break;

                  case Types.BYTE:
                     params[i] = new Byte(args[i].getByte());
                     break;

                  case Types.CHAR:
                     params[i] = new Character(args[i].getChar());
                     break;

                  case Types.FLOAT:
                     params[i] = new Float(args[i].getFloat());
                     break;

                  case Types.DOUBLE:
                     params[i] = new Double(args[i].getDouble());
                     break;

                  case Types.BOOLEAN:
                     params[i] = new Boolean(args[i].getBoolean());
                     break;

                  case Types.ARGS:
                     params[i] = args[i].getObject();
                     break;

                  case Types.VARIANT:
                     params[i] = args[i];
                     break;

               }
            }
            else {
               Class compType = p.component;
               StringTokenizer st = new StringTokenizer(args[i].getString(), "\u0001");
               int len = st.countTokens();
               Object arr = Array.newInstance(compType, len);

               switch ( p.type ) {

                  case Types.STRING:
                     for ( int j = 0; j < len; j++ )
                        Array.set(arr, j, st.nextToken());
                     break;

                  case Types.LONG:
                     for ( int j = 0; j < len; j++ )
                        Array.setLong(arr, j, Long.parseLong(st.nextToken()));
                     break;

                  case Types.INT:
                     for ( int j = 0; j < len; j++ )
                        Array.setInt(arr, j, Integer.parseInt(st.nextToken()));
                     break;

                  case Types.SHORT:
                     for ( int j = 0; j < len; j++ )
                        Array.setShort(arr, j, Short.parseShort(st.nextToken()));
                     break;

                  case Types.BYTE:
                     for ( int j = 0; j < len; j++ )
                        Array.setByte(arr, j, Byte.parseByte(st.nextToken()));
                     break;

                  case Types.CHAR:
                     for ( int j = 0; j < len; j++ )
                        Array.setChar(arr, j, st.nextToken().charAt(0));
                     break;

                  case Types.FLOAT:
                     for ( int j = 0; j < len; j++ )
                        Array.setFloat(arr, j, Float.parseFloat(st.nextToken()));
                     break;

                  case Types.DOUBLE:
                     for ( int j = 0; j < len; j++ )
                        Array.setDouble(arr, j, Double.parseDouble(st.nextToken()));
                     break;

                  case Types.BOOLEAN:
                     for ( int j = 0; j < len; j++ )
                        Array.setBoolean(arr, j, isTrue(st.nextToken()));
                     break;

                  case Types.VARIANT:
                     for ( int j = 0; j < len; j++ )
                        Array.set(arr, j, new Variant(st.nextToken()));
                     break;

               }
               params[i] = arr;
            }
         }
         try {
            return info.method.invoke(obj, params);
         }
         catch ( Exception e ) {
            throw new XMLObjectRuntimeException("Error Invoking Method '"+name+"'");
         }
      }
      else
         throw new MethodNotFoundException("Method '"+name+"' Not Found");
   }

   /**
    * invoke invokes the named method against the specified Reflectable Object,
    * passing to the method the set of specified parameters.  This method will
    * retrieve parameters by name from the Properties set, and convert them to
    * a String array before invoking.
    *
    * @param obj The target Reflectable
    * @param name The method name
    * @param params The parameter values
    * @return The result of the method
    * @throws XMLObjectException if an invocation error occurs
    */
   public Object invoke(Reflectable obj, String name, Args params) throws XMLObjectException {
      MethodInfo info = (MethodInfo)methods.get(name);
      if ( info != null ) {
         Variant[] args = new Variant[info.params.length];
         for ( int i = 0; i < info.params.length; i++ ) {
            if ( info.params[i].type == Types.ARGS )
               args[i] = new Variant(params);
            else
               args[i] = params.get(info.params[i].name);
         }
         return invoke(obj, name, args);
      }
      else
         throw new MethodNotFoundException("Method '"+name+"' Not Found");
   }

   /**
    * invoke invokes the named method against the specified Reflectable Object,
    * passing to the method the set of specified parameters.  This method will
    * retrieve parameters by name from the element's attribute set, and convert
    * them to a String array before invoking.
    *
    * @param obj The target Reflectable
    * @param name The method name
    * @param element The Element to use for Attributes
    * @return The result of the method
    * @throws XMLObjectException if an invocation error occurs
    */
   public Object invoke(Reflectable obj, String name, Element element) throws XMLObjectException  {
      return invoke(obj, name, new ElementArgs(element));
   }

   /**
    * ParamInfo encapsulates parameter type and name information
    */

   private class ParamInfo {
      public Class component;
      public int type;
      public boolean array;
      public String name = null;
      public String defValue = null;
      public String errValue = null;

      public ParamInfo(String name, int type, Class component) {
         this.name = name;
         this.type = type;
         this.component = component;
         array = true;
      }

      public ParamInfo(String name, int type) {
         this.name = name;
         this.type = type;
         component = null;
         array = false;
      }
   }


   /**
    * MethodInfo encapsulates Method result, name, and parameter information
    */

   private class MethodInfo {
      public int result;
      public String name;
      public Method method;
      public ParamInfo[] params;

      public MethodInfo(int result, String name, Method method, ParamInfo[] params) {
         this.result = result;
         this.name = name;
         this.method = method;
         this.params = params;
      }
   }
}

