/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.content.crosswalk;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import org.apache.commons.lang.ArrayUtils;
import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.DCValue;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.content.Site;
import org.dspace.content.authority.Choices;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.PluginManager;
import org.dspace.handle.HandleManager;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.Verifier;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jdom.transform.XSLTransformException;
import org.jdom.transform.XSLTransformer;

/**
 * Configurable XSLT-driven dissemination Crosswalk
 * <p>
 * See the XSLTCrosswalk superclass for details on configuration.
 * <p>
 * <h3>Additional Configuration of Dissemination crosswalk:</h3>
 * The disseminator also needs to be configured with an XML Namespace
 * (including prefix and URI) and an XML Schema for output format.  This
 * is configured on additional properties in the DSpace Configuration, i.e.:
 * <pre>
 *   crosswalk.dissemination.<i>PluginName</i>.namespace.<i>Prefix</i> = <i>namespace-URI</i>
 *   crosswalk.dissemination.<i>PluginName</i>.schemaLocation = <i>schemaLocation value</i>
 *   crosswalk.dissemination.<i>PluginName</i>.preferList = <i>boolean</i> (default is false)
 * </pre>
 * For example:
 * <pre>
 *   crosswalk.dissemination.qdc.namespace.dc = http://purl.org/dc/elements/1.1/
 *   crosswalk.dissemination.qdc.namespace.dcterms = http://purl.org/dc/terms/
 *   crosswalk.dissemination.qdc.schemaLocation = \
 *      http://purl.org/dc/elements/1.1/ http://dublincore.org/schemas/xmls/qdc/2003/04/02/qualifieddc.xsd
 *   crosswalk.dissemination.qdc.preferList = true
 * </pre>
 *
 * @author Larry Stone
 * @author Scott Phillips
 * @author Pascal-Nicolas Becker
 * @version $Revision$
 * @see XSLTCrosswalk
 */
public class XSLTDisseminationCrosswalk
    extends XSLTCrosswalk
    implements DisseminationCrosswalk
{
    /** log4j category */
    private static Logger log = Logger.getLogger(XSLTDisseminationCrosswalk.class);
    
    /** DSpace context, will be created if XSLTDisseminationCrosswalk had been started by command-line. */
    private static Context context;

    private static final String DIRECTION = "dissemination";

    private static String aliases[] = makeAliases(DIRECTION);

    public static String[] getPluginNames()
    {
        return (String[]) ArrayUtils.clone(aliases);
    }

    // namespace and schema; don't worry about initializing these
    // until there's an instance, so do it in constructor.
    private String schemaLocation = null;

    private Namespace namespaces[] = null;

    private boolean preferList = false;

    // load the namespace and schema from config
    private void init()
        throws CrosswalkInternalException
    {
        if (namespaces != null || schemaLocation != null)
        {
            return;
        }
        String myAlias = getPluginInstanceName();
        if (myAlias == null)
        {
            log.error("Must use PluginManager to instantiate XSLTDisseminationCrosswalk so the class knows its name.");
            throw new CrosswalkInternalException("Must use PluginManager to instantiate XSLTDisseminationCrosswalk so the class knows its name.");
        }

        // all configs for this plugin instance start with this:
        String prefix = CONFIG_PREFIX+DIRECTION+"."+myAlias+".";

        // get the schema location string, should already be in the
        // right format for value of "schemaLocation" attribute.
        schemaLocation = ConfigurationManager.getProperty(prefix+"schemaLocation");
        if (schemaLocation == null)
        {
            log.warn("No schemaLocation for crosswalk="+myAlias+", key="+prefix+"schemaLocation");
        }
        
        // sanity check: schemaLocation should have space.
        else if (schemaLocation.length() > 0 && schemaLocation.indexOf(' ') < 0)
        {
            log.warn("Possible INVALID schemaLocation (no space found) for crosswalk="+
                      myAlias+", key="+prefix+"schemaLocation"+
                      "\n\tCorrect format is \"{namespace} {schema-URL}\"");
        }

        // grovel for namespaces of the form:
        //  crosswalk.diss.{PLUGIN_NAME}.namespace.{PREFIX} = {URI}
        String nsPrefix = prefix + "namespace.";
        Enumeration<String> pe = (Enumeration<String>)ConfigurationManager.propertyNames();
        List<Namespace> nsList = new ArrayList<Namespace>();
        while (pe.hasMoreElements())
        {
            String key = pe.nextElement();
            if (key.startsWith(nsPrefix))
            {
                nsList.add(Namespace.getNamespace(key.substring(nsPrefix.length()),
                        ConfigurationManager.getProperty(key)));
            }
        }
        namespaces = nsList.toArray(new Namespace[nsList.size()]);

        preferList = ConfigurationManager.getBooleanProperty(prefix+"preferList", false);
    }

    /**
     * Return the namespace used by this crosswalk.
     *
     * @see DisseminationCrosswalk
     */
    public Namespace[] getNamespaces()
    {
        try
        {
            init();
        }
        catch (CrosswalkInternalException e)
        {
            log.error(e.toString());
        }
        return (Namespace[]) ArrayUtils.clone(namespaces);
    }

    /**
     * Return the schema location used by this crosswalk.
     *
     * @see DisseminationCrosswalk
     */
    public String getSchemaLocation()
    {
        try
        {
            init();
        }
        catch (CrosswalkInternalException e)
        {
            log.error(e.toString());
        }
        return schemaLocation;
    }

    /**
     * Disseminate the DSpace item, collection, or community.
     *
     * @see DisseminationCrosswalk
     */
    public Element disseminateElement(DSpaceObject dso)
        throws CrosswalkException,
               IOException, SQLException, AuthorizeException
    {
        int type = dso.getType();
        if (!(type == Constants.ITEM ||
              type == Constants.COLLECTION ||
              type == Constants.COMMUNITY))
        {
            throw new CrosswalkObjectNotSupported("XSLTDisseminationCrosswalk can only crosswalk items, collections, and communities.");
        }

        init();

        XSLTransformer xform = getTransformer(DIRECTION);
        if (xform == null)
        {
            throw new CrosswalkInternalException("Failed to initialize transformer, probably error loading stylesheet.");
        }

        try
        {
            Document ddim = new Document(createDIM(dso));
            Document result = xform.transform(ddim);
            Element root = result.getRootElement();
            root.detach();
            return root;
        }
        catch (XSLTransformException e)
        {
            log.error("Got error: "+e.toString());
            throw new CrosswalkInternalException("XSL translation failed: "+e.toString(), e);
        }
    }

    /**
     * Disseminate the DSpace item, collection, or community.
     *
     * @see DisseminationCrosswalk
     */
    public List<Element> disseminateList(DSpaceObject dso)
        throws CrosswalkException,
               IOException, SQLException, AuthorizeException
    {
        int type = dso.getType();
        if (!(type == Constants.ITEM ||
              type == Constants.COLLECTION ||
              type == Constants.COMMUNITY))
        {
            throw new CrosswalkObjectNotSupported("XSLTDisseminationCrosswalk can only crosswalk a items, collections, and communities.");
        }

        init();

        XSLTransformer xform = getTransformer(DIRECTION);
        if (xform == null)
        {
            throw new CrosswalkInternalException("Failed to initialize transformer, probably error loading stylesheet.");
        }

        try
        {
            return xform.transform(createDIM(dso).getChildren());
        }
        catch (XSLTransformException e)
        {
            log.error("Got error: "+e.toString());
            throw new CrosswalkInternalException("XSL translation failed: "+e.toString(), e);
        }
    }

    /**
     * Determine is this crosswalk can dessiminate the given object.
     *
     * @see DisseminationCrosswalk
     */
    public boolean canDisseminate(DSpaceObject dso)
    {
        return dso.getType() == Constants.ITEM;
    }

    /**
     * return true if this crosswalk prefers the list form over an single
     * element, otherwise false.
     *
     * @see DisseminationCrosswalk
     */
    public boolean preferList()
    {
        try
        {
            init();
        }
        catch (CrosswalkInternalException e)
        {
            log.error(e.toString());
        }
        return preferList;
    }

    /**
     * Generate an intermediate representation of a DSpace object.
     *
     * @param dso The dspace object to build a representation of.
     */
    public static Element createDIM(DSpaceObject dso, DCValue[] dcvs)
    {
        Element dim = new Element("dim", DIM_NS);
        String type = Constants.typeText[dso.getType()];
        dim.setAttribute("dspaceType",type);

        for (int i = 0; i < dcvs.length; i++)
        {
            DCValue dcv = dcvs[i];
            Element field =
            createField(dcv.schema, dcv.element, dcv.qualifier,
                        dcv.language, dcv.value, dcv.authority, dcv.confidence);
            dim.addContent(field);
        }
        return dim;
    }

    /**
     * Generate an intermediate representation of a DSpace object.
     *
     * @param dso The dspace object to build a representation of.
     */
    public static Element createDIM(DSpaceObject dso)
    {
        if (dso.getType() == Constants.ITEM)
        {
            Item item = (Item) dso;
            return createDIM(dso, item.getMetadata(Item.ANY, Item.ANY, Item.ANY, Item.ANY));
        }
        else
        {
            Element dim = new Element("dim", DIM_NS);
            String type = Constants.typeText[dso.getType()];
            dim.setAttribute("dspaceType",type);

            if (dso.getType() == Constants.COLLECTION)
            {
                Collection collection = (Collection) dso;

                String description = collection.getMetadata("introductory_text");
                String description_abstract = collection.getMetadata("short_description");
                String description_table = collection.getMetadata("side_bar_text");
                String identifier_uri = "hdl:" + collection.getHandle();
                String provenance = collection.getMetadata("provenance_description");
                String rights = collection.getMetadata("copyright_text");
                String rights_license = collection.getMetadata("license");
                String title = collection.getMetadata("name");

                dim.addContent(createField("dc","description",null,null,description));
                dim.addContent(createField("dc","description","abstract",null,description_abstract));
                dim.addContent(createField("dc","description","tableofcontents",null,description_table));
                dim.addContent(createField("dc","identifier","uri",null,identifier_uri));
                dim.addContent(createField("dc","provenance",null,null,provenance));
                dim.addContent(createField("dc","rights",null,null,rights));
                dim.addContent(createField("dc","rights","license",null,rights_license));
                dim.addContent(createField("dc","title",null,null,title));
            }
            else if (dso.getType() == Constants.COMMUNITY)
            {
                Community community = (Community) dso;

                String description = community.getMetadata("introductory_text");
                String description_abstract = community.getMetadata("short_description");
                String description_table = community.getMetadata("side_bar_text");
                String identifier_uri = "hdl:" + community.getHandle();
                String rights = community.getMetadata("copyright_text");
                String title = community.getMetadata("name");

                dim.addContent(createField("dc","description",null,null,description));
                dim.addContent(createField("dc","description","abstract",null,description_abstract));
                dim.addContent(createField("dc","description","tableofcontents",null,description_table));
                dim.addContent(createField("dc","identifier","uri",null,identifier_uri));
                dim.addContent(createField("dc","rights",null,null,rights));
                dim.addContent(createField("dc","title",null,null,title));
            }
            else if (dso.getType() == Constants.SITE)
            {
                Site site = (Site) dso;

                String identifier_uri = "hdl:" + site.getHandle();
                String title = site.getName();
                String url = site.getURL();

                //FIXME: adding two URIs for now (site handle and URL), in case site isn't using handles
                dim.addContent(createField("dc","identifier","uri",null,identifier_uri));
                dim.addContent(createField("dc","identifier","uri",null,url));
                dim.addContent(createField("dc","title",null,null,title));
            }
            // XXX FIXME: Nothing to crosswalk for bitstream?
            return dim;
        }
    }


     /**
     * Create a new DIM field element with the given attributes.
     *
     * @param schema The schema the DIM field belongs to.
     * @param element The element the DIM field belongs to.
     * @param qualifier The qualifier the DIM field belongs to.
     * @param language The language the DIM field belongs to.
     * @param value The value of the DIM field.
     * @return A new DIM field element
     */
    private static Element createField(String schema, String element, String qualifier, String language, String value)
    {
        return createField(schema, element, qualifier, language, value, null, -1);
    }

    /**
     * Create a new DIM field element with the given attributes.
     *
     * @param schema The schema the DIM field belongs to.
     * @param element The element the DIM field belongs to.
     * @param qualifier The qualifier the DIM field belongs to.
     * @param language The language the DIM field belongs to.
     * @param value The value of the DIM field.
     * @param authority The authority
     * @param confidence confidence in the authority
     * @return A new DIM field element
     */
    private static Element createField(String schema, String element, String qualifier, String language, String value,
                                        String authority, int confidence)
    {
        Element field = new Element("field",DIM_NS);
        field.setAttribute("mdschema",schema);
        field.setAttribute("element",element);
        if (qualifier != null)
        {
            field.setAttribute("qualifier", qualifier);
        }
        if (language != null)
        {
            field.setAttribute("lang", language);
        }

        field.setText(checkedString(value));

        if (authority != null)
        {
            field.setAttribute("authority", authority);
            field.setAttribute("confidence", Choices.getConfidenceText(confidence));
        }

        return field;
    }

    // Return string with non-XML characters (i.e. low control chars) excised.
    private static String checkedString(String value)
    {
        if (value == null)
        {
            return null;
        }
        String reason = Verifier.checkCharacterData(value);
        if (reason == null)
        {
            return value;
        }
        else
        {
            if (log.isDebugEnabled())
            {
                log.debug("Filtering out non-XML characters in string, reason=" + reason);
            }
            StringBuffer result = new StringBuffer(value.length());
            for (int i = 0; i < value.length(); ++i)
            {
                char c = value.charAt(i);
                if (Verifier.isXMLCharacter((int)c))
                {
                    result.append(c);
                }
            }
            return result.toString();
        }
    }
    
    /**
     * Simple command-line rig for testing the DIM output of a stylesheet.
     * Usage:  java XSLTDisseminationCrosswalk  <crosswalk-name> <handle> [output-file]
     */
    public static void main(String[] argv) throws Exception
    {
        log.error("started.");
        if (argv.length < 2 || argv.length > 3)
        {
            System.err.println("Usage:  java XSLTDisseminationCrosswalk <crosswalk-name> <handle> [output-file]");
            log.error("You started Dissemination Crosswalk Test/Export with a wrong number of parameters.");
            System.exit(1);
        }
        
        String xwalkname = argv[0];
        String handle = argv[1];
        OutputStream out = System.out;
        if (argv.length > 2)
        {
            try
            {
                out = new FileOutputStream(argv[2]);
            }
            catch (FileNotFoundException e)
            {
                System.err.println("Can't write to the specified file: " + e.getMessage());
                System.err.println("Will write output to stdout.");
            }
        }
        
        DisseminationCrosswalk xwalk = (DisseminationCrosswalk)PluginManager.getNamedPlugin(
                DisseminationCrosswalk.class, xwalkname);
        if (xwalk == null)
        {
            System.err.println("Error: Cannot find a DisseminationCrosswalk plugin for: \"" + xwalkname + "\"");
            log.error("Cannot find the Dissemination Crosswalk plugin.");
            System.exit(1);
        }
        
        context = new Context();
        context.turnOffAuthorisationSystem();
        
        DSpaceObject dso = null;
        try
        {
            dso = HandleManager.resolveToObject(context, handle);
        }
        catch (SQLException e)
        {
            System.err.println("Error: A problem with the database connection occurred, check logs for further information.");
            System.exit(1);
        }
        
        if (null == dso)
        {
            System.err.println("Can't find a DSpaceObject with the handle \"" + handle + "\"");
            System.exit(1);
        }
        
        if (!xwalk.canDisseminate(dso))
        {
            System.err.println("Dissemination Crosswalk can't disseminate this DSpaceObject.");
            log.error("Dissemination Crosswalk can't disseminate this DSpaceObject.");
            System.exit(1);
        }
        
        Element root = null;
        try
        {
            root = xwalk.disseminateElement(dso);
        }
        catch (Exception e)
        {
            // as this script is for testing dissemination crosswalks, we want
            // verbose information in case of an exception.
            System.err.println("An error occurred while processing the dissemination crosswalk.");
            System.err.println("=== Error Message ===");
            System.err.println(e.getMessage());
            System.err.println("===  Stack Trace  ===");
            e.printStackTrace();
            System.err.println("=====================");
            log.error("Caught: " + e.toString() + ".");
            log.error(e.getMessage());
            log.error(e.getStackTrace());
            System.exit(1);
        }
    
        try
        {
            XMLOutputter xmlout = new XMLOutputter(Format.getPrettyFormat());
            xmlout.output(new Document(root), out);
        }
        catch (Exception e)
        {
            // as this script is for testing dissemination crosswalks, we want
            // verbose information in case of an exception.
            System.err.println("An error occurred after processing the dissemination crosswalk.");
            System.err.println("The error occurred while trying to print the generated XML.");
            System.err.println("=== Error Message ===");
            System.err.println(e.getMessage());
            System.err.println("===  Stack Trace  ===");
            System.err.println(e.getStackTrace());
            System.err.println("=====================");
            log.error("Caught: " + e.toString() + ".");
            log.error(e.getMessage());
            log.error(e.getStackTrace());
            System.exit(1);
        }
        
        context.complete();
        if (out instanceof FileOutputStream)
        {
            out.close();
        }
    }
}
