// AbstractMobyRenderer.java
//
// Created: February 2006
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.dashboard.renderers;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.Utils;
import org.biomoby.shared.parser.MobyParser;
import org.biomoby.shared.datatypes.MapDataTypesIfc;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

import java.util.HashMap;
import java.util.Map;
import java.util.HashSet;
import java.util.List;
import java.util.Iterator;
import java.lang.reflect.Method;

/**
 * A class containing common utilities for renderers that process
 * Biomoby data types. <p>
 *
 * A common feature for renderers rendering Biomoby data types is that
 * they often have a list of data types they can deal with, together
 * with method names that can extract data from such data types.
 * Important is that the renderers must be able to render also for
 * those Biomoby data types that are not explicitly named in such
 * lists but inheriting from those who are named there. For finding
 * these relationship, we have here common utilities
 * <tt>isSubclassOfKnown</tt> (used usually in {@link #canHandle},
 * <tt>getMethodOfKnown</tt> and <tt>traverseToString</tt>. <p>
 *
 * The available renderers and how they map with Biomoby data types
 * and their methods are stored in a renderers configuration XML file
 * "rendereres.conf.xml". Here is an example of such file:
 *
 *<pre>
 *&lt;renderers&gt;
 *  &lt;renderer name="org.biomoby.service.dashboard.renderers.Base64Image"&gt;
 *    &lt;moby_type name="Image_PNG" method="get_imagedata"/&gt;
 *    &lt;moby_type name="b64_encoded_gif" method="get_content"/&gt;
 *    &lt;moby_type name="b64_Encoded_PNG" method="get_content"/&gt;
 *    &lt;moby_type name="b64_encoded_jpeg" method="get_content"/&gt;
 *    &lt;moby_type name="NCBI_Blast_XML_Gif" method="getMoby_hitGraph"/&gt;
 *  &lt;/renderer&gt;
 *  &lt;renderer name="org.biomoby.service.dashboard.renderers.Base64Image"&gt;
 *    &lt;moby_type name="GCP_GermplasmPedigreeTree" method="get_pedigree_xml"/&gt;
 *  &lt;/renderer&gt;
 *&lt;/renderers&gt;
 *</pre>
 *
 * Note that information from this file is used recursively, until a
 * proper value is found. For example, above the data type
 * "NCBI_Blast_XML_Gif" has method "getMoby_hitGraph" that returns an
 * object representing Biomoby data "b64_encoded_jpeg" that will be
 * again looked for in the same file (its method "get_content" already
 * returns type String). <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: AbstractMobyRenderer.java,v 1.4 2006/06/28 16:28:30 senger Exp $
 */

public abstract class AbstractMobyRenderer
    extends AbstractRenderer {

    private static org.apache.commons.logging.Log log =
	org.apache.commons.logging.LogFactory.getLog (AbstractMobyRenderer.class);

    protected static final String RENDERERS_CONF_FILE = "renderers.conf.xml";

    protected static MapDataTypesIfc mapDataTypes;

    /**
     * Contains (for each class sepaately) recognisable Biomoby data
     * type names as keys, and method names that should be called on
     * them as values. The searching this map and calling methods will
     * go on until a method returning a String is called (and it is
     * done by calling 'traverseToString()).
     *
     * The map is created when this class is loaded the first time,
     * from an XML configuration file RENDERERS_CONF_FILE (found
     * on the classpath as a resource of this class).
     */
    static protected Map KNOWN;
    static {
	try {
	    KNOWN = new HashMap();
	    SAXBuilder builder = new SAXBuilder();
	    Document doc =
		builder.build (Utils.getResourceURL (RENDERERS_CONF_FILE,
						     Renderer.class));
	    Element root = doc.getRootElement();
	    List rs = root.getChildren ("renderer");
	    for (Iterator it = rs.iterator(); it.hasNext(); ) {
		Element rend = (Element)it.next();
		String className = rend.getAttributeValue ("class");
		try {
		    Class aClass = Class.forName (className);
		    List mts = rend.getChildren ("moby_type");
		    Map aClassMap = new HashMap();
		    for (Iterator it2 = mts.iterator(); it2.hasNext(); ) {
			Element mobyType = (Element)it2.next();
			String mtName = mobyType.getAttributeValue ("name");
			String mtMethod = mobyType.getAttributeValue ("method");
			if (mtName != null && mtMethod != null)
			    aClassMap.put (mtName, mtMethod);
		    }
		    KNOWN.put (aClass, aClassMap);
		} catch (Exception e2) {
		    log.error ("Class instantiation failed for " +
			       className + ": " +
			       Utils.stackTraceIfSerious (e2));
		}
	    }
	} catch (Exception e) {
	    log.error ("Problem with " + RENDERERS_CONF_FILE + ": " +
		       Utils.stackTraceIfSerious (e));
	}
	if (log.isDebugEnabled()) {
	    for (Iterator it = KNOWN.keySet().iterator(); it.hasNext(); ) {
		Class aClass = (Class)it.next();
		log.debug ("Moby types renderable by " + aClass.getName() +
			   ": " + KNOWN.get (aClass));
	    }
	}
    }

    /*********************************************************************
     *
     ********************************************************************/
    public AbstractMobyRenderer (String name) {
        super (name);
    }

    /*********************************************************************
     *
     ********************************************************************/
    public AbstractMobyRenderer (String name, String iconName) {
        super (name, iconName);
    }

    /*********************************************************************
     * Search the global KNOWN (where are relationships of all
     * available renderers to the various Biomoby data types) and
     * return a Map (containing Biomoby data types pointing to method
     * names, as usual in KNOWN) for this particular class.
     *
     * If you do not want to get renderers from the global KNOWN,
     * overwrite this class, and return something else.
     ********************************************************************/
    protected Map getKnownTypesForThisClass() {
	Class thisClass = this.getClass();
	for (Iterator it = KNOWN.keySet().iterator(); it.hasNext(); ) {
	    Class parentClass = (Class)it.next();
	    if (parentClass.isAssignableFrom (thisClass))
		return (Map)KNOWN.get (parentClass);
	}
	return new HashMap();
    }

    /*********************************************************************
     * Return true if the 'testedTypeName' is in the global map KNOWN
     * (as a key) - for this class, or is a subtype of anything (of
     * any key) in KNOWN (again, for this class).
     ********************************************************************/
    protected boolean isSubclassOfKnown (String testedTypeName) {
	Map knownTypes = getKnownTypesForThisClass();
	try {
	    if (mapDataTypes == null)
		mapDataTypes = MobyParser.loadB2JMapping();
	    Class testedClass = mapDataTypes.getClass (testedTypeName);
	    if (testedClass == null)
		return false;
	    for (Iterator it = knownTypes.keySet().iterator(); it.hasNext(); ) {
		String dataTypeName = (String)it.next();
		Class parentClass = mapDataTypes.getClass (dataTypeName);
		if (parentClass == null) {
		    log.error ("No class found for " + dataTypeName +
			       ". Try to regenerate datatypes.");
		} else {
		    if (parentClass.isAssignableFrom (testedClass))
			return true;
		}
	    }
	} catch (MobyException e) {
	}
	return false;
    }

    /*********************************************************************
     * Return a method name that can be called on an 'actor' - because
     * such method was found in KNOWN, for this class (it is there as
     * a value). It can be found in KNOWN either because the moby type
     * name corresponding to 'actor' is directly in KNOWN (as a key),
     * or there is its parent type. Return null if nothing found.
     ********************************************************************/
    protected String getMethodOfKnown (Object actor) {
	Map knownTypes = getKnownTypesForThisClass();
	try {
	    if (mapDataTypes == null)
		mapDataTypes = MobyParser.loadB2JMapping();
	    Class actorClass = actor.getClass();
	    for (Iterator it = knownTypes.keySet().iterator(); it.hasNext(); ) {
		String dataTypeName = (String)it.next();
		Class parentClass = mapDataTypes.getClass (dataTypeName);
		if (parentClass == null)
		    continue;
		if (parentClass.isAssignableFrom (actorClass))
		    return (String)knownTypes.get (dataTypeName);
	    }
	} catch (MobyException e) {
	}
	return null;
    }

    /**************************************************************************
     * Return a string that represents a value of a known - for this
     * class - Biomoby data type (all known types and methods that can
     * be called on them are in a global variable KNOWN). This is how:
     *
     * 1) Find a moby data type corresponding to 'data' in
     *    KNOWN (for this class). Take there a method name.
     *
     * 2) Call this method. If a return type is String, you are done.
     *
     * 3) Otherwise, use the result as a new 'data' and go back to
     *    step 1. Be carefull, not to cycle indefinitely: remember
     *    what object types were already visited.
     *
     * @throws MobyException if it cannot be done
     **************************************************************************/
    protected String traverseToString (Object data)
	throws MobyException {

	try {
	    HashSet alreadyVisited = new HashSet();

	    Object actor = data;
	    while (true) {
		Class actorClass = actor.getClass();

		// be careful
		if (alreadyVisited.contains (actorClass))
		    throw new MobyException ("Found cycle for class " + actorClass +
					     ": " + alreadyVisited.toString());
		alreadyVisited.add (actorClass);

		// find the method name
		String methodName = getMethodOfKnown (actor);
		if (methodName == null)
		    throw new MobyException ("Expected method was not found in classes: " +
					     alreadyVisited.toString());

		// call it
		Method method = actorClass.getMethod (methodName, new Class[] { });
		Object result = method.invoke (actor, new Object[] { });

		// satisfied?
		if (result instanceof String)
		    return (String)result;
		actor = result;
	    }

	} catch (Throwable e) {
	    throw new MobyException (e.toString());
	}
    }

}
