/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.enterprise.tools.guiframework.view;

// JAXP packages
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import org.w3c.dom.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.xml.sax.SAXException;
import org.w3c.dom.Document;

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.StringTokenizer;

import com.sun.enterprise.tools.guiframework.FrameworkDescriptor;
import com.sun.enterprise.tools.guiframework.event.descriptors.EventDescriptor;
import com.sun.enterprise.tools.guiframework.event.descriptors.HandlerDescriptor;
import com.sun.enterprise.tools.guiframework.event.descriptors.IODescriptor;
import com.sun.enterprise.tools.guiframework.event.descriptors.UseHandlerDescriptor;
import com.sun.enterprise.tools.guiframework.exception.FrameworkException;
import com.sun.enterprise.tools.guiframework.view.descriptors.ViewDescriptor;
import com.sun.enterprise.tools.guiframework.util.Util;


/**
 *
 */
public class ViewXMLReader {

    public ViewXMLReader(String fileName, String dtdURLBase) throws Exception {
	this(fileName, dtdURLBase, null);
    }
    
    public ViewXMLReader(String fileName, String dtdURLBase, String jspRoot) throws Exception {
	this(new FileInputStream(new File(fileName)), dtdURLBase, jspRoot);
    }

    public ViewXMLReader(InputStream is, String dtdURLBase) throws Exception {
	// Invoke w/ the default "/jsp/" jsp root
	this(is, dtdURLBase, null);
    }

    public ViewXMLReader(InputStream is, String dtdURLBase, String jspRoot) throws Exception {
        // Invoke w/ the entityresolver
        this(is, dtdURLBase, jspRoot, null);
    }
    
    public ViewXMLReader(InputStream is, String dtdURLBase, String jspRoot, 
        EntityResolver entityResolver) throws Exception {
	setJSPRoot(jspRoot);
	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);
	dbf.setValidating(false);
	dbf.setIgnoringComments(false);
	dbf.setIgnoringElementContentWhitespace(false);
	dbf.setCoalescing(false);
	// The opposite of creating entity ref nodes is expanding them inline
	dbf.setExpandEntityReferences(true);
	
	DocumentBuilder db = dbf.newDocumentBuilder();
        if (entityResolver != null) {
            db.setEntityResolver(entityResolver);
        }
	OutputStreamWriter errorWriter = new OutputStreamWriter(System.err, OUTPUT_ENCODING);
	db.setErrorHandler(new MyErrorHandler(new PrintWriter(errorWriter, true)));
	doc = db.parse(is, dtdURLBase);
	preProcess();
    }


    private void preProcess() throws Exception {
	// get all display item types
	NodeList nodeList = doc.getElementsByTagName(DISPLAY_ITEM_TYPE);
	for (int i = 0; i < nodeList.getLength(); i++) {
	    Element elem = (Element)nodeList.item(i);
	    typeMap.put(elem.getAttribute(NAME), elem);
	}
    }


    /**
     *
     */
    private ViewDescriptor getDescriptor(String name, String type) throws Exception {
	// I need to create a DisplayFieldDescriptor. The name of the class is known.
	Element typeNode = (Element)typeMap.get(type);
	if (typeNode == null) {
	    return new ViewDescriptor(name); // default type
	}

	// Get the ViewDescriptor Class
	Class descriptorClass =
	    Class.forName(typeNode.getAttribute(DESCRIPTOR_CLASS));

	// Get the constructor for the ViewDescriptor
	Constructor constructor =
	    descriptorClass.getConstructor(new Class[] {String.class});

	// Create a new instance
	return (ViewDescriptor)constructor.newInstance(new Object[] {name});
    }


    /**
     *
     */
    private void processEvent(Element node, ViewDescriptor vd) throws Exception {
	String type = node.getAttribute(TYPE);
	EventDescriptor eventDesc = new EventDescriptor(vd, type);

	UseHandlerDescriptor useDesc;
	for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
	    if (child.getNodeName().compareToIgnoreCase(CALL) == 0) {
		useDesc = getUseHandler(eventDesc, (Element)child);
		eventDesc.addEventHandler(useDesc);
	    }
	}
	vd.setEventDescriptor(eventDesc);
    }


    /**
     *
     */
    private UseHandlerDescriptor getUseHandler(FrameworkDescriptor parent, Element node) {
	// Get the Handler Node
	Element handlerNode = getHandlerNode(node.getAttribute(NAME), doc);
	if (handlerNode == null) {
	    throw new IllegalArgumentException("'"+CALL+
		"' referred to a handler that was not found: '"+
		node.getAttribute(NAME)+"'");
	}

	// Create the UseHandlerDescriptor
	HandlerDescriptor handlerDesc = getHandler(handlerNode);
	UseHandlerDescriptor useDesc =
	    new UseHandlerDescriptor(parent, handlerDesc);

	// Get the 'if' value
	String ifValue = node.getAttribute(IF);
	if (Util.hasValue(ifValue)) {
	    useDesc.setIfCheck(ifValue);
	}

	// Get the input parameters
	Element elt;
	for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
	    String childName = child.getNodeName();
	    if (childName.compareToIgnoreCase(INPUT)==0) {
		elt = (Element)child;
		// Make sure 'name' is valid...
		String name = elt.getAttribute(NAME);
		if (handlerDesc.getInputDescriptor(name) == null) {
		    throw new FrameworkException(CALL+" '"+
			handlerDesc.getName()+"' defined "+INPUT+": '"+name+
			"', however, '"+name+"' is not a valid "+INPUT+"!");
		}
		useDesc.setInputValue(name, getParameterValue(elt));
	    } else if (childName.compareToIgnoreCase(OUTPUT)==0) {
		elt = (Element)child;
		// Make sure 'name' is valid...
		String name = elt.getAttribute(NAME);
		if (handlerDesc.getOutputDescriptor(name) == null) {
		    throw new FrameworkException(CALL+" '"+
			handlerDesc.getName()+"' defined "+OUTPUT+": '"+name+
			"', however, '"+name+"' is not a valid "+OUTPUT+"!");
		}
		useDesc.setOutputMapping(
		    name, elt.getAttribute(TARGET_KEY), elt.getAttribute(TARGET_TYPE));
	    }
	}

	// Return the UseHandlerDescriptor
	return useDesc;
    }


    /**
     *
     */
    private HandlerDescriptor getHandler(Element node) {
	// Make sure we haven't already created this handler
	String name = node.getAttribute(NAME);
	HandlerDescriptor handler = (HandlerDescriptor)handlerMap.get(name);
	if (handler != null) {
	    return handler;
	}

	// Create Handler
	handler = new HandlerDescriptor(name);

	// Set the description (hold this for future tool support)
	handler.setDescription(node.getAttribute(DESCRIPTION));

	// Set method
	String className = node.getAttribute(FUNCTION_CLASS);
	String methodName = node.getAttribute(FUNCTION_METHOD);
	if (Util.hasValue(className) && Util.hasValue(methodName)) {
	    handler.setHandlerMethod(className, methodName);
	}

	// Set If Check
	String ifValue = node.getAttribute(IF);
	if (Util.hasValue(ifValue)) {
	    handler.setIfCheck(ifValue);
	}

	// Iterate through the kids...
	String nodeName;
	UseHandlerDescriptor useDesc;
	IODescriptor ioDesc;
	for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
	    nodeName = child.getNodeName();
	    if (nodeName.compareToIgnoreCase(CALL) == 0) {
		useDesc = getUseHandler(handler, (Element)child);
		handler.addChildHandlerDescriptor(useDesc);
	    } else if (nodeName.compareToIgnoreCase(INPUT_DEF) == 0) {
		ioDesc = getIODescriptor((Element)child);
		String defValue = ((Element)child).getAttribute(DEFAULT);
		if (Util.hasValue(defValue)) {
		    ioDesc.setDefault(defValue);
		}
		handler.addInputDescriptor(ioDesc);
	    } else if (nodeName.compareToIgnoreCase(OUTPUT_DEF) == 0) {
		ioDesc = getIODescriptor((Element)child);
		handler.addOutputDescriptor(ioDesc);
	    }
	}

	// Return the handler
	handlerMap.put(name, handler);
	return handler;
    }


    /**
     *
     */
    private IODescriptor getIODescriptor(Element node) {
	IODescriptor desc = new IODescriptor(
	    node.getAttribute(NAME), node.getAttribute(TYPE));
	desc.setDescription(node.getAttribute(DESCRIPTION));
	return desc;
    }


    private void processParameter(Element node, ViewDescriptor vd) {
	vd.addParameter(node.getAttribute(NAME), getParameterValue(node));
    }


    private void processInclude(Element node, ViewDescriptor vd) throws Exception{
	/* look for the displayItem in the xml and just add it to the vd */
	Node descriptorNode = getViewDescriptorNode(node.getAttribute("item"), doc);
	if (descriptorNode == null) {
	    throw new Exception ("Invalid include :" + node.getAttribute("item"));
	}

	// Process Child DisplayItem
	vd.addChildDescriptor(process((Element)descriptorNode));
    }

    private Object getInputValue(Element node) {
	return getParameterValue(node);
    }

    private Object getParameterValue(Element node) {
	if (Util.hasValue(node.getAttribute(VALUE))) {
	    /* removed support for $(top) parsing at xml parsing time
            return Util.replace(node.getAttribute(VALUE), RESERVED_WORD_TOP, topLevelDescriptorName);
             */
            return node.getAttribute(VALUE);
	}

	ArrayList values = new ArrayList();
	for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
	    if (child.getNodeName().equalsIgnoreCase(VALUES)) {
	    /* removed support for $(top) parsing at xml parsing time
		String str = Util.replace(((Element)child).getAttribute(VALUE), RESERVED_WORD_TOP, topLevelDescriptorName);
             */
		String str = ((Element)child).getAttribute(VALUE);
		values.add(str);
	    }
	}

	// Make sure they specified one or the other.
	if (values.size() == 0) {
	    return null;
	}
	return values;
    }


    private String getDisplayItem(Node node) {
	Node parentNode = node;
	while (true) {
	    parentNode = parentNode.getParentNode();
	    if (parentNode == null || parentNode.getNodeName().compareToIgnoreCase(DISPLAY_ITEM)==0) {
		break;
	    }
	}
	if (parentNode == null) {
	    return null;
	}
	NamedNodeMap attrs = parentNode.getAttributes();
	if (attrs == null) {
	    return null;
	}
	return attrs.getNamedItem(NAME).getNodeValue();
    }


    private ViewDescriptor process(Element node) throws Exception {
	String name         = node.getAttribute(NAME);
	String description  = node.getAttribute(DESCRIPTION);
	String type         = node.getAttribute(TYPE);
	String extendsFrom  = node.getAttribute("extends");
	String displayURL   = node.getAttribute("displayURL");

	// Perform "extends" functionality
	Node extendsViewDescriptor = null;
	if (Util.hasValue(extendsFrom)) {
	    extendsViewDescriptor = getViewDescriptorNode(extendsFrom, doc);
	    if (extendsViewDescriptor == null) {
		throw new Exception("Cannot find " + extendsFrom);
	    }
            // leaving this for backward compatibility - type should better
            // be specified in the view descriptor itself..
	    if (type == null || type.length() == 0) {
		type = ((Element)extendsViewDescriptor).getAttribute(TYPE);
	    }
	}

	ViewDescriptor vd = getDescriptor(name, type);
	vd.setDescription(description);

	// The following line sets a reasonable default display URL
	if ((displayURL == null) || (displayURL.equals(""))) {
	    vd.setDisplayURL(getJSPName(name));
	} else {
	    vd.setDisplayURL(displayURL);
	}

	// Perform "extends" functionality
	if (extendsViewDescriptor != null) {
            processExtends((Element)extendsViewDescriptor, vd);
        }
//	    processDisplayItemChildren((Element)extendsViewDescriptor, vd);
//	}

	processDisplayItemChildren(node ,vd);
	return vd;
    }

    protected void processExtends(Element extendsViewDescriptor, 
                ViewDescriptor vd) throws Exception {
	String extendsFrom  = extendsViewDescriptor.getAttribute("extends");
        if (Util.hasValue(extendsFrom)) {
	    Element extendsParentViewDescriptor = getViewDescriptorNode(extendsFrom, doc);
	    if (extendsViewDescriptor == null) {
		throw new Exception("Cannot find " + extendsFrom);
	    }
            processExtends(extendsParentViewDescriptor, vd);
        }
        processDisplayItemChildren((Element)extendsViewDescriptor, vd);
    }

    protected String getJSPName(String name) {
	return getJSPRoot() + name + ".jsp";
    }


    protected String getJSPRoot() {
	return _jspRoot;
    }


    protected void setJSPRoot(String root) {
	if (root == null) {
	    return;
	}
	_jspRoot = root;
    }


    private void processDisplayItemChildren(Node node, ViewDescriptor vd) throws Exception {
	for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
	    String childName = child.getNodeName();
	    if (childName.compareToIgnoreCase(PARAMETER)==0) {
		processParameter((Element)child, vd);
	    }
	    else if (childName.compareToIgnoreCase(EVENTS)==0) {
		processEvent((Element)child, vd);
	    }
	    else if (childName.compareToIgnoreCase(DISPLAY_ITEM)==0) {
		// Process Child DisplayItem
		vd.addChildDescriptor(process((Element)child));
	    }
	    else if (childName.compareToIgnoreCase(INCLUDE)==0) {
		processInclude((Element)child, vd);
	    }

	}
    }


    /**
     *
     */
    private Element getHandlerNode(String key, Node node) {
	if (node == null) {
	    return null;
	}
	for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
	    String nodeName = child.getNodeName();
	    if (nodeName.compareToIgnoreCase(FUNCTION)==0) {
		if (((Element)child).getAttribute(NAME).equals(key)) {
		    return (Element)child;
		}
	    } else {
		Element handlerNode = getHandlerNode(key, child);
		if (handlerNode != null) {
		    return handlerNode;
		}
	    }
	}
	return null;
    }


    private Element getViewDescriptorNode(String key, Node node) throws Exception {
	if (node == null) {
	    return null;
	}

	// First try to find a Top-Level displayItem...
	for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
	    String nodeName = child.getNodeName();
	    if (nodeName.compareToIgnoreCase(DISPLAY_ITEM)==0) {
		NamedNodeMap attrs = child.getAttributes();
		if (attrs == null) {
		    throw new Exception("No attrs!!!");
		}
		Node name = attrs.getNamedItem(NAME);
		if (name == null) {
		    throw new Exception("Name cannot be null!!!");
		}
		if (name.getNodeValue().equals(key)) {
		    return (Element)child;
		}
	    }
	    else {
		Element vdNode = getViewDescriptorNode(key, child);
		if (vdNode != null) {
		    return vdNode;
		}
	    }
	}

	// The above loop should have found it... unless we should be looking
	// for a deep ViewDescriptor...
	if (key.indexOf(".") >= 0) {
	    /*key could be a.b.c etc., which depicts a hierarchy of viewdescriptors
	     *in xml terms, this means
	     *<displayItem name = "a">
	     *  <displayItem name = "b">
	     *      <displayItem name = "c"/>
	     *  </displayItem>
	     *</displayItem> */
	    StringTokenizer tokenizer = new StringTokenizer(key, ".");
	    Node childNode = node;
	    while (tokenizer.hasMoreElements()) {
		key = tokenizer.nextToken();
		childNode = getViewDescriptorNode(key, childNode);
	    }
	    return (Element)childNode;
	}

	return null;
    }


    /**
     *
     */
    public ViewDescriptor getViewDescriptor(String key) throws Exception {
	Element node = getViewDescriptorNode(key, doc);
	if (node == null) {
	    return null;
//	    throw new FrameworkException("Key not found in XML file: "+key);
	}
	//topLevelDescriptorName = key;
	return process(node);
    }


    // Error handler to report errors and warnings
    private static class MyErrorHandler implements ErrorHandler {
	/** Error handler output goes here */
	private PrintWriter out;

	MyErrorHandler(PrintWriter out) {
	    this.out = out;
	}

	/**
	 * Returns a string describing parse exception details
	 */
	private String getParseExceptionInfo(SAXParseException spe) {
	    String systemId = spe.getSystemId();
	    if (systemId == null) {
		systemId = "null";
	    }
	    String info = "URI=" + systemId +  " Line=" + spe.getLineNumber() +  ": " + spe.getMessage();
	    return info;
	}

	// The following methods are standard SAX ErrorHandler methods.
	// See SAX documentation for more info.

	public void warning(SAXParseException spe) throws SAXException {
	    out.println("Warning: " + getParseExceptionInfo(spe));
	}

	public void error(SAXParseException spe) throws SAXException {
	    String message = "Error: " + getParseExceptionInfo(spe);
	    throw new SAXException(message);
	}

	public void fatalError(SAXParseException spe) throws SAXException {
	    String message = "Fatal Error: " + getParseExceptionInfo(spe);
	    throw new SAXException(message);
	}
    }


    // returns all 'top level' view descriptors
    public HashMap getAllViewDescriptors() throws Exception {
	NodeList nodeList = doc.getElementsByTagName(DISPLAY_ITEM);
        HashMap descriptors = new HashMap();
	for (int i = 0; i < nodeList.getLength(); i++) {
	    Element displayItem = (Element)nodeList.item(i);
            Node parent = displayItem.getParentNode();
            // if it has a parent, it is not a top level view descriptor.
            if (parent != null && parent.getNodeName().equals(DISPLAY_ITEM))
                continue;
            String topLevelDescriptorName = displayItem.getAttribute(NAME);
            if (descriptors.get(topLevelDescriptorName) != null) {
                throw new Exception("DisplayItem  " + topLevelDescriptorName + " defined more than once");
            }
            descriptors.put(topLevelDescriptorName, process(displayItem));
        }
        return descriptors;
    }
    
    public static void main(String args[]) {
	try {
	    if (Array.getLength(args) < 2) {
		System.out.println("Usage ViewXMLReader <XMLFileName> <ViewName>");
		return;
	    }
	    ViewXMLReader xmlReader = new ViewXMLReader(args[0], "file://./");
	    xmlReader.getViewDescriptor(args[1]);
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
    }


    private String	_jspRoot = "/jsp/";
    //private String	topLevelDescriptorName;


    private Document doc;
    private Map typeMap		= new HashMap();
    private Map handlerMap	= new HashMap();

    private static final String OUTPUT_ENCODING	    = "UTF-8";

    private static final String DESCRIPTOR_CLASS    = "descriptorClass";
    private static final String DISPLAY_ITEM	    = "displayItem";
    private static final String DISPLAY_ITEM_TYPE   = "displayItemType";
    private static final String EVENTS		    = "events";
    private static final String FUNCTION	    = "function";
    private static final String FUNCTION_CLASS	    = "className";
    private static final String FUNCTION_METHOD	    = "methodName";
    private static final String INPUT_DEF	    = "inputDef";
    private static final String OUTPUT_DEF	    = "outputDef";
    private static final String CALL		    = "call";
    private static final String INPUT		    = "input";
    private static final String OUTPUT		    = "output";
    private static final String TARGET_KEY	    = "key";
    private static final String TARGET_TYPE	    = "target";
    private static final String IF		    = "if";
    private static final String PARAMETER	    = "parameter";
    private static final String INCLUDE		    = "include";
    private static final String VALUE		    = "value";
    private static final String VALUES		    = "values";
    private static final String NAME		    = "name";
    private static final String TYPE		    = "type";
    private static final String DEFAULT		    = "default";
    private static final String DESCRIPTION	    = "description";
}
