package org.mandarax.xkb.framework;

/**
 *  Copyright (C) 1999-2004  Jens Dietrich (mailto:mandarax@jbdietrich.com)
 *
 *  This library 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 of the License, or (at your option) any later version.
 *
 *  This library 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 this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jdom.Element;
import org.mandarax.kernel.LogicFactory;
import org.mandarax.xkb.XKBException;
/**
 * An adapter class for objects. Strings and primitives are just stringified,
 * dates represented by the time as long, classes by the name, and
 * beans are handeled via introspection. <p>
 * <strong>Warning:</strong> We cannot and do not solve the more general problem of XML object serialization here.
 * Instead, this is a 80-20 solution that might work fine for simple object models. For more sophisticated
 * object models, you will need to implement your own object adapter. In particular, we do not handle circular 
 * references.
 * <p>
 * In the future (once JDK 1.4 is the common platform) we will integrate the new XML serialization mechanism!
 * @author <A HREF="mailto:mandarax@jbdietrich.com">Jens Dietrich</A>
 * @version 3.3.1 <9 August 2004>
 * @since 1.6 
 */
public class XMLAdapter4Objects extends CachedXMLAdapter {
      public static final String OBJECT = "object";
      public static final String DATA = "data";
      public static final String OBJECT_TYPE = "object_type";
      public static final String STRING = String.class.getName();
      public static final String INTEGER = Integer.class.getName();
      public static final String BOOLEAN = Boolean.class.getName();
      public static final String LONG = Long.class.getName();
      public static final String SHORT = Short.class.getName();
      public static final String CHARACTER = Character.class.getName();
      public static final String BYTE = Byte.class.getName();
      public static final String DOUBLE = Double.class.getName();
      public static final String FLOAT = Float.class.getName();
      public static final String BEAN = "bean";
      public static final String PRIMITIVE = "primitive";
      public static final String DATE = "date";
      public static final String NULL = "null";
      public static final String PROPERTIES = "properties";
      public static final String PROPERTY_NAME = "property_name";
      public static final String BEAN_CLASS = "bean_class";
      public static final String CLASS = "class";

/**
 * Export an object, i.e., convert it to an element in the DOM.
 * @param obj an object
 * @param driver the generic driver
 * @param cache a cache used in order to associate the same
 * id with various occurences of the same object
 * @exception an XKBException is thrown if export fails
 */
protected Element _exportObject(Object obj,GenericDriver driver,Map cache) throws  XKBException {

      boolean debug = LOG_XKB.isDebugEnabled();
      Element e = new Element(OBJECT);
      // export null, primitives and strings first
      if (obj==null) {
      		e.setAttribute(OBJECT_TYPE,NULL);
      		return e;
      }
      if (isSimpleObject(obj)) {
            e.setAttribute(DATA,obj.toString());
            e.setAttribute(OBJECT_TYPE,PRIMITIVE);
            e.setAttribute(CLASS,obj.getClass().getName());
            if (debug) LOG_XKB.debug("Exporting object as primitive - value: " + obj.toString());
            return e;
      }
      // export classes
      if (obj instanceof Class) {
            e.setAttribute(DATA,((Class)obj).getName());
            e.setAttribute(OBJECT_TYPE,CLASS);
            if (debug) LOG_XKB.debug("Exporting object as class - value: " + ((Class)obj).getName());
            return e;
      }
      // export dates
      if (obj instanceof Date) {
            Date date = (Date)obj;
            e.setAttribute(DATA,String.valueOf(date.getTime()));
            e.setAttribute(OBJECT_TYPE,DATE);
            e.setAttribute(CLASS,obj.getClass().getName());
            if (debug) LOG_XKB.debug("Exporting object as date - value: " + date.getTime());
            return e;
      }
      // otherwise export object as a bean using introspection
      e.setAttribute(OBJECT_TYPE,BEAN);
      e.setAttribute(CLASS,obj.getClass().getName());
      try {
            BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass(),Object.class);
            PropertyDescriptor[] properties = beanInfo.getPropertyDescriptors();
            Element eProperties = new Element(PROPERTIES);
            if (debug) LOG_XKB.debug("Exporting object as bean - class: " + obj.getClass());
            for (int i=0;i<properties.length;i++) {
                  PropertyDescriptor property = properties[i];
                  String name = property.getName();
                  Method getter = property.getReadMethod();
                  Object[] par = new Object[0];
                  try {
                        Object value = getter.invoke(obj,par);
                        if (debug) LOG_XKB.debug("Exporting property : " + name + " -> " + value);
                        Element eValue = exportObject(value,driver,cache);
                        eValue.setAttribute(PROPERTY_NAME,name);
                        eProperties.addContent(eValue);
                  }
                  catch (Exception x) {
                        // note: we carry on without this property but log the error !
                        LOG_XKB.error("Cannot write property " + name + " for object " + obj,x);
                  }
            }
            e.addContent(eProperties);
      }
      catch (Exception x) {
            throw new XKBException("Export failed: cannot inspect object " + obj);
      }
      return e;



}
/**
 * Build an object from an XML element.
 * @param e an element
 * @param driver the generic driver
 * @param cache a cache used to identify objects that have the same id
 * @param lfactory the logic factory used to create objects
 * @exception an XKBException is thrown if export fails
 */
protected Object _importObject(Element e,GenericDriver driver,Map cache,LogicFactory lfactory) throws XKBException {
      // import null and primitives first
      String objType = e.getAttributeValue(OBJECT_TYPE);
      if (NULL.equals(objType)) return null;      
      String className = e.getAttributeValue(CLASS);
      String stringified = e.getAttributeValue(DATA);
      if (PRIMITIVE.equals(objType)) {
            if (STRING.equals(className)) return stringified;
            if (BOOLEAN.equals(className)) return Boolean.valueOf(stringified);
            if (CHARACTER.equals(className)) return new Character(stringified.charAt(0));
            // the numerical types
            try {
                  if (INTEGER.equals(className)) return Integer.valueOf(stringified);
                  if (BYTE.equals(className)) return Byte.valueOf(stringified);
                  if (SHORT.equals(className)) return Short.valueOf(stringified);
                  if (LONG.equals(className)) return Long.valueOf(stringified);
                  if (DOUBLE.equals(className)) return Double.valueOf(stringified);
                  if (FLOAT.equals(className)) return Float.valueOf(stringified);
            }
            catch (NumberFormatException x) {
                  throw new XKBException("Cannot parse " + stringified + " to instanciate " + objType);
            }
      }
      // import classes
      if (CLASS.equals(objType)) {
            String clazzName = e.getAttributeValue(DATA);
            try {
                  return Class.forName(clazzName);
            }
            catch (Exception x) {
                  throw new XKBException("Cannot find class named " + clazzName);
            }
      }
      // import dates
      if (DATE.equals(objType)) {
            long time = 0;
            try {
                  time = Long.parseLong(e.getAttributeValue(DATA));
            }
            catch (NumberFormatException x) {
                  throw new XKBException("Cannot interprete " + time + " as a date (cannot parse long)");
            }
            if (className == null) return new Date(time);
            if (java.sql.Date.class.getName().equals(className)) return new java.sql.Date(time);
            if (java.sql.Time.class.getName().equals(className)) return new java.sql.Time(time);
            if (java.sql.Timestamp.class.getName().equals(className)) return new java.sql.Timestamp(time);

            LOG_XKB.warn("Don't know implementation class " + className +" for date, use java.util.Date");
            return new Date(time);
      }
      // import beans
      if (BEAN.equals(objType)) {
            Object bean = null;
            try {
                  Class clazz = Class.forName(className);
                  bean = clazz.newInstance();
                  BeanInfo beanInfo = Introspector.getBeanInfo(clazz,Object.class);
                  PropertyDescriptor[] properties = beanInfo.getPropertyDescriptors();
                  List eProperties = e.getChild(PROPERTIES).getChildren();
                  PropertyDescriptor property = null;
                  Object[] par = new Object[1];
                  for (Iterator it = eProperties.iterator();it.hasNext();) {
                        property = null;
                        Element eProperty = (Element)it.next();
                        String name = eProperty.getAttributeValue(PROPERTY_NAME);
                        // look up properties
                        for (int i=0;i<properties.length;i++) {
                              if (properties[i].getName().equals(name)) property = properties[i];
                        }
                        // if no property has been found, warn but continue
                        if (property==null) LOG_XKB.warn("Introspection problem - cannot locate property " + name + " in class " + clazz);
                        else {
                              try {
                                    Method setter = property.getWriteMethod();
                                    par[0] = importObject(eProperty,driver,cache,lfactory);
                                    setter.invoke(bean,par);
                              }
                              catch (Exception x) {
                                    // note: we carry on without this property but log the error !
                                    LOG_XKB.error("Cannot set property " + name + " for object " + bean,x);
                              }
                        }
                  }
                  return bean;
            }
            catch (Exception x) {
            	  LOG_XKB.error("Cannot import bean of class " + className + " from data " + bean,x);	
                  throw new XKBException("Cannot import object " + bean);
            }
     }
     else throw new XKBException("Cannot import object from " + e.toString() + " - it is neither marked as a primitive nor as a bean");
}
/**
 * Get the name of the associated tag (element).
 * @return a string
 */
public String getTagName() {
      return OBJECT;
}
/**
 * Get the kind of object the adapter can export/import.
 * @return a string
 */
public String getKindOfObject() {
      return GenericDriver.OBJECT;
}
/**
 * Indicates whether the object represents a primitive or string.
 * @return a boolean
 * @param obj an object
 */
private boolean isSimpleObject(Object obj) {
      return (obj instanceof String) ||
      (obj instanceof Number) ||
      (obj instanceof Boolean);
}
}
