/*
 * Created on 2 mai 2005
 *
 * TODO To change the template for this generated file go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
package org.injac.oai.server.crosswalk;


import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import sun.misc.BASE64Encoder;
import org.apache.log4j.Logger;
import org.apache.xalan.templates.ElemElement;
import org.injac.oai.util.IdentityTransformer;
import org.w3c.dom.*;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.oclc.oai.server.crosswalk.Crosswalk;
import org.oclc.oai.server.verb.CannotDisseminateFormatException;

import com.icl.saxon.expr.*;
import com.icl.saxon.Context;
import com.icl.saxon.om.*;
/**
 * @author François Jannin
 *
 * Generic Crosswalk for generating metadatas from a format XML file
 */
public class WebdavCrosswalk extends Crosswalk{
	/**
	 * Static logger
	 */
	static Logger logger = Logger.getLogger(WebdavCrosswalk.class);
	private String rootTag = null;
	private String formatFile = null;
	private String metadataPrefix = null;
	private String schema  = null; 
	private String metadataNamespace = null;
	private HashMap nameSpaces = null;
	/**
	 * Document for output metadata xml fragment
	 */
	private MetadataDocument  metaDoc = null;
	/**
	 * Document for the format mapping
	 */
	private Document formatDoc=null;
    /**
     * The constructor assigns the schemaLocation associated with this crosswalk. Since
     * the crosswalk is trivial in this case, no properties are utilized.
     *
     * @param properties properties that are needed to configure the crosswalk.
     */
    public WebdavCrosswalk(String formatfile, String schemaLoc) {
	//"http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"
    	super(schemaLoc);
    	formatFile = formatfile;
    	initContext(formatfile);
	
    }

    /**
     * Can this nativeItem be represented in this format?
     * @param nativeItem a record in native format
     * @return true if DC format is possible, false otherwise.
     */
    public  boolean isAvailableFor(Object nativeItem) {
	
	if (nativeItem != null)
	    return true;
	return false;
    }

	 /**
     * Perform the actual crosswalk.
     *
     * @param nativeItem the native "item". In this case, it is
     * already formatted as an OAI <record> element, with the
     * possible exception that multiple metadataFormats are
     * present in the <metadata> element.
     * @return a String containing the XML to be stored within the <metadata> element.
     * @exception CannotDisseminateFormatException nativeItem doesn't support this format.
     */
    public  String createMetadata(Object nativeItem)
	throws CannotDisseminateFormatException {
    	try {
    			// build metadata from xml map file
    			return parseFormat(nativeItem);
    	}catch(Exception e)
		{
    		//logger.error("Webdav2oai_dc::createMetadata : "+e.getMessage());
    		throw new CannotDisseminateFormatException(metadataPrefix);
		}
    	
    	
    }
	private void initContext(String formatfile)
	{
		
        //   Create DocumentBuilderFactory  
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        
        // Configure it to ignore comments
        factory.setIgnoringComments(true);
        // Create the builder and parse the file
        
        try{
        	//   Load the specified XML document
        	DocumentBuilder builder = factory.newDocumentBuilder();
       
        	formatDoc = builder.parse(new File(formatfile));
        	Element el =formatDoc.getDocumentElement();
        	// get mapping attributes
        	metadataPrefix = el.getAttribute("metadata-prefix"); 
        	schema = el.getAttribute("schema"); 
        	metadataNamespace= el.getAttribute("metadata-namespace"); 
        	rootTag=el.getAttribute("root-tag");
        	nameSpaces = readNameSpaces(formatDoc);
        }catch(Exception ex)
		{
        	logger.error("WebdavCrosswalk::initContext :"+ex.getMessage());
		}
        
        metaDoc = new MetadataDocument(rootTag, metadataPrefix, metadataNamespace, schema, nameSpaces);
        logger.debug("create dom doc with root :"+ rootTag);
	
	}
	/**
	 * Read namespaces from map file and build namespace declarations for OAI metadata part.
	 * @param doc xml map file
	 * @return namespace declarations as a string
	 */
	private HashMap readNameSpaces(Document doc)
	{
		HashMap map =new HashMap();
		NodeList mapNodes = formatDoc.getElementsByTagName("namespace");
    	for(int nodeIndex = 0; nodeIndex < mapNodes.getLength();nodeIndex++)
        {
          try
          {
          	Element el = (Element)mapNodes.item(nodeIndex);;
          	String prefix = el.getAttribute("prefix");
			String namespace = el.getAttribute("uri");
			map.put(prefix, namespace);
			
          } catch(Exception exp)
          {
          		logger.error(exp.getMessage()+"\nnamespaces :"+map.toString());
                return null;
          }
          
        }   
        logger.debug("namespaces :"+map.toString());
        return map;
	}
	/** 
	 * Parse map file and build metadatas from nativeItem
	 * @param nativeItem
	 * @return metadata element of a record as string
	 */
	private String parseFormat(Object nativeItem)throws CannotDisseminateFormatException
	{
		
		
		logger.debug("entering parseFormat");
		// Namespace declarations and schema validity
		String xmlns = " xmlns:"+metadataPrefix+"=\""+metadataNamespace+"\""; 
    	xmlns+="\n"+nameSpaces; 
    	xmlns+="\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""; 
    	xmlns+="\n xsi:schemaLocation=\""+schema+"\"";
    	String result = "<"+rootTag+xmlns+">";//getSchemaLocation();
    	boolean canDisseminate = true;
    	HashMap hash = (HashMap)nativeItem;
        //
        try
        {
        	NodeList mapNodes = formatDoc.getElementsByTagName("mapping-attr");
        	ArrayList tagList = new ArrayList();
        	for(int nodeIndex = 0; nodeIndex < mapNodes.getLength();nodeIndex++)
            {
              try
              {
              	 Node curResultNode = mapNodes.item(nodeIndex);
              	 Element el=(Element)curResultNode;
              	 String sourceName = el.getAttribute("source");
				 String destFormatName = el.getAttribute("dest");
				 String required=el.getAttribute("required");
				 String multiParse=el.getAttribute("multiParse");
				 String attributes=el.getAttribute("attributes");
				 
                 // get webdav value from hashmap
                 String S = (String)hash.get(sourceName);
                
                 	
                 // test disseminating possibility with required args
				 if((required!=null) && (required.equals("yes")))
				 {
				 	canDisseminate= canDisseminate && (S != null && S.length() > 0); 
				 }
				 // special date conversion	
                 if(destFormatName.equals("etdms:date"))
                 	S = UTCtoETDMSDate(S);
                 
                 logger.debug("webdavName :" + sourceName +"\t desFormatName : "+destFormatName);
                 if(S != null && S.length() > 0) 
                 {
                 	if(multiParse.equals(""))
                 	{	
                 		DestTag tag = parseDestFormatName(destFormatName, S, attributes);
                 		tagList.add(tag);
                 	}
                 	else {
                 		StringTokenizer st = new StringTokenizer(S, multiParse);
                 		while(st.hasMoreTokens())
                 		{
                 			DestTag tag= parseDestFormatName(destFormatName, st.nextToken(), attributes);
                 			tagList.add(tag);
                 		}
                 	}
                 		
                 }
              }
              catch(Exception exp)
              {
              	logger.error(exp.toString());
                throw new CannotDisseminateFormatException(metadataPrefix);
              }
           }
        	result=mergeTagList(tagList);
			//result +="</"+rootTag+">";
        }catch(Exception exp)
        {
        	logger.error(exp.getClass().getName()+ " : "+ exp.getMessage());
            throw new CannotDisseminateFormatException(metadataPrefix);
        	//result +=  "</"+rootTag+">";
            //return result;
        }
        if(canDisseminate)
        	return result;
        else
        	throw new CannotDisseminateFormatException(metadataPrefix);
	}
	private String UTCtoETDMSDate(String utc){
		if(utc ==null)
			return null;
		String res = utc.substring(0,10);
		return res;
	}
	/**
	 * Parse a XPATH and convert it into XML tags structure with value on the inner element
	 * @param dest
	 * @param value
	 * @return xml fragment
	 */
	private DestTag parseDestFormatName(String dest, String value, String attributes) {
		logger.debug("entering parseDestFormat : dest :" + dest);
		HashMap attrMap =null;
		if(!attributes.equals(""))
        {
         	//parseAttributes(S);
		 	attrMap = new HashMap();
		 	value= parseAttributes(attributes, value, attrMap);
        }
		int last=dest.lastIndexOf("/");
		DestTag tag = new DestTag();
		String prefix="";
		int pref_index= dest.indexOf(":");
		if(pref_index!=-1)
			prefix=dest.substring(0,pref_index);
		// no path tag
		if(last==-1)
		{
			logger.debug("no path tag");
			tag.prefix = prefix;
			tag.tag = dest.substring(prefix.length()+1);
			tag.XBase="";
			tag.value=value;
			tag.attrMap = attrMap;
			
		}
		// tag with path
		else
		{	
			logger.debug("tag with path");
			tag.prefix = prefix;
			tag.tag = dest.substring(last+1);
			tag.XBase=dest.substring(pref_index+1, last);
			tag.value=value;
			tag.attrMap = attrMap;
			StringTokenizer st = new StringTokenizer(tag.XBase,"/");
		// 	build list of elements
			ArrayList elements= new ArrayList();
			while(st.hasMoreElements())
			{
				String path_element = (String)st.nextToken();
				elements.add(path_element);
				logger.debug("parseDestFormat : path element :" + path_element);
			}
			tag.XBaseArray = elements;
		}
		String debug = "parseDestFormat for " +dest;
		debug+=tag;
		logger.debug("parseDestFormat for:"+dest+tag);
		return tag;
	}
	/**
	 * Parse attributes
	 * @param attributes string with attributes names to parse 
	 * @param value String to parse 
	 * @param en empty HashMap 
	 * @return new value without accodances
	 * 
	 */
	private String parseAttributes(String attributes, String value, HashMap attrMap){
		
		int accIndex=value.indexOf("{");
		int endIndex=value.indexOf("}");
	 	if(accIndex !=-1 && endIndex!= -1)
	 	{
	 		// parse names
	 		ArrayList attrNames = new ArrayList();
	 		StringTokenizer stNames = new StringTokenizer(attributes,",");
	 		while(stNames.hasMoreTokens()){
	 			attrNames.add(stNames.nextToken());
	 		}
	 		// parse values
	 		ArrayList attrValues = new ArrayList();
	 		
	 		String values = value.substring(accIndex+1, endIndex);
	 		StringTokenizer stValues = new StringTokenizer(values,",");
	 		while(stValues.hasMoreTokens()){
	 			attrValues.add(stValues.nextToken());
	 		}
	 		// fill hashmap
	 		if(attrValues.size()<= attrNames.size()){
	 			for(int i=0;i<attrValues.size();i++)
		 		{
		 			attrMap.put(attrNames.get(i), attrValues.get(i));
		 		}
	 		}
	 		else {
	 			logger.error("parseAttributes : too many values exceed attributes number");
	 		}
	 		// remove accodances from value
	 		value=value.substring(endIndex+1);
	 		
	 	}	
	 	return value;
	}
	
	
	/**
	 * Generate XML structure from Destination tags
	 * @param tagList
	 * @return
	 */
	private String mergeTagList(ArrayList tagList){
		logger.debug("entering mergeTagList array size = "+tagList.size());
		StringBuffer result = new StringBuffer();
		// replace global by local for multithread issues
		MetadataDocument ametaDoc = new MetadataDocument(rootTag, metadataPrefix, metadataNamespace, schema, nameSpaces);
		
		// sort tags by path depth
		//Collections.sort(tagList);
		int tagIndex=0;
		Document doc = null;
		Element root = null;
		Element current = null;
		
	try{	
		logger.debug("clear dom doc with root :"+ rootTag);
		ametaDoc.clear();	
		logger.debug("cleared doc :"+ IdentityTransformer.TransformToString(doc));
		for(tagIndex=0;tagIndex < tagList.size();tagIndex++)
		{
			
			DestTag tag= (DestTag)tagList.get(tagIndex);
			logger.debug("mergeTagList : \n"+ "_____ mergeTagList _____ "+tag);
			doc = ametaDoc.getDocument();
			root = doc.getDocumentElement();
			current= root;
			if(tag.XBaseArray != null){
//				 create the remaining tree 
				for(int x=0;x<tag.XBaseArray.size();x++)
				{
					String nodeName= (String) tag.XBaseArray.get(x);
					NodeList nodes =current.getElementsByTagName(tag.prefix+":"+nodeName);
					// create node if not already existing
					if(nodes == null || nodes.getLength()==0)
					{
						logger.debug("create dom element :"+ nodeName);
						Element newElement = doc.createElement(tag.prefix+":"+nodeName);
						//newElement.setPrefix(tag.prefix);
						current = (Element)current.appendChild(newElement);
					}
					else
						// or we reuse the existing one
					{
						current = (Element)nodes.item(0);
					}
				}
			}
			
			// insert tag with its value
			logger.debug("create leaf : "+ tag.prefix+":"+tag.tag+"\tvalue : "+tag.value);
			Element leaf = doc.createElement(tag.prefix+":"+tag.tag);
			leaf.appendChild(doc.createTextNode(tag.value));
			//leaf.appendChild(doc.createTextNode(Base64.encode(tag.value.getBytes("utf-8"))));
			// append attributes
			if(tag.attrMap != null)
			{
				Iterator attribs = tag.attrMap.keySet().iterator();
				while(attribs.hasNext()){
					String name= (String)attribs.next();
					String value = (String)tag.attrMap.get(name);
					leaf.setAttribute(name,value);		
				}
				
			}
			current.appendChild(leaf);
		}
		// remove xml decl
		
		result.append(IdentityTransformer.TransformToString(doc));
		int xmlStart = result.indexOf("<?");
		if(xmlStart!=-1)
		{
			result.replace(xmlStart, result.indexOf(">")+1,"");
		}
		
	}catch(Exception exp){
		logger.error(exp.getClass().getName()+ " : "+ exp.getMessage()+ "result : "+result);
	}
		return result.toString();
	}
	
	
	/**
	 * inner class for describing a Destination tag
	 * @author F.Jannin
	 *
	 * TODO make constructor, accessors with trigger for countpath recomputing  
	 */
	
	class DestTag implements Comparable {
		public String prefix;
		public String XBase;
		public String tag;
		public String value;
		public ArrayList XBaseArray=null;
		public HashMap attrMap=null;
		
		/**
		 * return 0 if Xbase path are the same
		 * return -1 if path is nearer from root element
		 * return +1 if path is farer from root element 
		 */
		public int compareTo(Object o) {
			if(countPath() < ((DestTag)o).countPath())
				return -1;
			if(countPath() > ((DestTag)o).countPath())
				return 1;
			return 0;
		}
		private int countPath(){
			StringTokenizer st = new StringTokenizer(XBase,"/");
			int count=0;
			while(st.hasMoreElements())
				count++;
			return count;
		}
		public String toString(){
			String result="";
			result+="\nprefix :" + this.prefix;
			result+="\ntag :" + this.tag;
			result+="\nXBase :" + this.XBase;
			result+="\nvalue :" + this.value;
			if(this.XBaseArray != null)
				result+="\narray size :" + this.XBaseArray.size();
			else
				result+="\nNULL array";
			return result;
		}
	}
}
