/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2003-2004 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * 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.1 of the License, or 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.deployment.tests;

// java
import java.io.Reader;
import java.io.StringReader;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.StringTokenizer;
import javax.xml.namespace.QName;

import org.ow2.jonas.deployment.common.DeploymentDescException;
import org.ow2.jonas.deployment.common.xml.AbsElement;
import org.ow2.jonas.deployment.common.xml.Element;
import org.ow2.jonas.deployment.common.xml.Qname;

// jonas

/**
 * Defines an abstract class for testing the classes built with Digester
 * @author Florent Benoit
 */
public abstract class AbsDeploymentTest {

    /**
     * Counter for each kind of element
     */
    private Hashtable elementsCounter = null;

    /**
     * Build a new Test
     */
    protected AbsDeploymentTest() {
        elementsCounter = new Hashtable();
    }

    /**
     * Create an xml structure and then parse the resulting xml and check the
     * result No validation is done if random is set to true
     * @throws Exception if the stress test is not successfull
     */
    public void stress() throws Exception {

        startTest(false);

        for (int i = 0; i < 100; i++) {
            startTest(true);
        }
    }

    // Abstract Methods used by startTest implemented in subclasses

    public abstract AbsElement getTopLevelElement() throws Exception;

    public abstract String parse(Reader reader, String name, boolean validation) throws Exception;

    /**
     * Defines the function for the specific test
     * @param random use random or not to fill elements
     * @throws Exception if the test failed
     */
    public void startTest(boolean random) throws Exception {
        AbsElement ae = getTopLevelElement();
        fill(ae, random);
        String xmlOriginal = ae.toXML();
        String xmlParsed = parse(new StringReader(xmlOriginal), "test", false);
        checkDiff(xmlOriginal, xmlParsed);

    }

    /**
     * Gets the xml after digester parsing
     * @throws Exception if the parsing fail
     */
    public void parseElement() throws Exception {
        AbsElement ae = getTopLevelElement();
        fill(ae, false);
        System.out.println("Parsing xml :");
        System.out.println(ae);
        String xmlOriginal = ae.toXML();
        String xmlParsed = parse(new StringReader(xmlOriginal), "test", false);
        System.out.println("Result = ");
        System.out.println(xmlParsed);
    }

    /**
     * parse with validation from an xml file
     * @throws Exception if the parsing fail
     */
    public void parseXmlfromFile(String fileName) throws Exception {
        FileReader fileReader = null;

        // Check if the file exists.
        if (!new File(fileName).exists()) {
            throw new DeploymentDescException("The file '" + fileName + "' was not found.");
        }
        fileReader = new FileReader(fileName);
        System.out.println("Parsing xml : " + fileName);
        String xmlParsed = parse(fileReader, fileName, true);
        System.out.println("Parsing OK Result = ");
        System.out.println(xmlParsed);
    }

    /**
     * Check the difference between original xml and parsed xml
     * @param xmlOriginal original XML
     * @param xmlParsed parsed XML
     * @throws Exception if there is a difference between original and parsed
     *         XML
     */
    protected void checkDiff(String xmlOriginal, String xmlParsed) throws Exception {
        StringTokenizer stOri = new StringTokenizer(xmlOriginal, "\n");
        StringTokenizer stPardsed = new StringTokenizer(xmlParsed, "\n");

        // Compare line by line
        while (stOri.hasMoreTokens() && stPardsed.hasMoreTokens()) {
            String lineOri = stOri.nextToken();
            String lineparsed = stPardsed.nextToken();
            if (!lineOri.equals(lineparsed)) {
                System.err.println("Failure in xml :");
                System.err.println(xmlOriginal);
                throw new Exception("Line : " + lineOri + " is different than parsed value: " + lineparsed);
            }
        }
    }

    /**
     * Fill the structure of the given element. Fill randomly if random is set
     * to true
     * @param element element to fill
     * @param random determines if the element must be filled randomly or not
     * @throws Exception if the element can not be filled
     */
    public void fill(Element element, boolean random) throws Exception {

        // Get setters of the element
        Method[] methods = element.getClass().getMethods();

        // Analyze if it is Set or Add Method
        for (int i = 0; i < methods.length; i++) {
            // Get name
            Method method = methods[i];
            String name = method.getName();
            Class[] argsMethod = null;
            argsMethod = method.getParameterTypes();

            // One argument method
            if ((argsMethod.length == 1) && (((name.startsWith("set") && (!name.equalsIgnoreCase("setHeader"))) || name.startsWith("add")))) {
                Class cl = argsMethod[0];
                if (cl.getName().equals("java.lang.String")) {
                    // Test if it's one argument with String type :
                    fillString(element, method, random);
                } else if (cl.getName().equals("javax.xml.namespace.QName")) {
                    fillQName(element, method, random);
                } else {
                    // It's a subelement
                    if (name.startsWith("set")) {
                        setElement(element, method, argsMethod, random);
                    } else {
                        addElement(element, method, argsMethod, random);
                    }
                }
            }

        }

    }

    /**
     * Random for returning true or false
     * @return true or false with random
     */
    protected boolean aleatOK() {
        return (Math.random() > 0.5);
    }

    /**
     * Gives a number between 0 and 5
     * @return a random intger number between 0 and 5
     */
    protected int nbAleat() {
        return (int) (Math.random() * 5);
    }

    /**
     * Add to an element its sub element Add many times a sub-element if random
     * is not set to true
     * @param element element on which we have to add sub elements
     * @param method method of the element (determine type of the sub element
     * @param argsMethod arguments of the method
     * @param random use random or not
     * @throws Exception if the subelement can not be added
     */
    protected void addElement(Element element, Method method, Class[] argsMethod, boolean random) throws Exception {
        int nb = 1;
        if (random) {
            nb = nbAleat();
        }
        for (int i = 0; i < nb; i++) {
            setElement(element, method, argsMethod, random);
        }
    }

    /**
     * Set the subelement of an element The subelement may not be set if random
     * is used
     * @param element element on which we have to add sub elements
     * @param method method of the element (determine type of the sub element
     * @param argsMethod arguments of the method
     * @param random use random or not
     * @throws Exception if the subelement can not be set
     */
    protected void setElement(Element element, Method method, Class[] argsMethod, boolean random) throws Exception {
        if (random && !aleatOK()) {
            return;
        }

        String name = method.getName();

        Class cl = argsMethod[0];
        String className = cl.getName();

        // Element object ?
        if (Element.class.isAssignableFrom(cl)) {
            // Create required object
            Object subElement = cl.newInstance();

            // Add object on top level object
            method.invoke(element, new Object[] {subElement});

            // Fill values of this element
            fill((Element) subElement, random);

            // Qname is a particular type, need to set the name
            if (cl.isAssignableFrom(Qname.class)) {
                // Transform name into Qname
                String qNameElement = convertUpperCaseToXml(name.substring(3, name.length()));
                ((Qname) subElement).setName(qNameElement);
            }

        }
    }

    /**
     * Set the string attribute of the given element
     * @param element element on which we have to set the string
     * @param method method of the element (determine type of the sub element
     * @param random use random or not
     * @throws Exception if the String attribute can not be added
     */
    protected void fillString(Element element, Method method, boolean random) throws Exception {
        if (random && !aleatOK()) {
            return;
        }
        method.invoke(element, (Object[]) new String[] {getNameCounterForElement(element, method)});
    }

    /**
     * Set the QName attribute of the given element
     * @param element element on which we have to set the string
     * @param method method of the element (determine type of the sub element
     * @param random use random or not
     * @throws Exception if the QName can not be set
     */
    protected void fillQName(Element element, Method method, boolean random) throws Exception {
        if (random && !aleatOK()) {
            return;
        }
        QName qName = new QName("prefix:Test", getNameCounterForElement(element, method));
        method.invoke(element, (Object[]) new QName[] {qName});
    }

    /**
     * Gives a Name + counter for a type of an element This is used to add
     * counter when adding xml attributes Only use in order to make easier the
     * read of the parsed XML file
     * @param element the given element for which we want a counter
     * @param method the name of the string to add
     * @return the Name + counter for the specified element type
     */
    protected String getNameCounterForElement(Element element, Method method) {
        StringBuffer sb = new StringBuffer();

        // Extract package.classname
        String name = method.getName().substring(3, method.getName().length());
        String packageClassname = element.getClass().getPackage().getName();
        String fullClass = packageClassname + "." + name;

        // Existing counter ?
        Integer counter = (Integer) elementsCounter.get(fullClass);
        if (counter == null) {
            // init counter as it don't already exist
            counter = new Integer(0);
        } else {
            // Increment
            counter = new Integer(counter.intValue() + 1);
        }
        elementsCounter.put(fullClass, counter);

        // return value
        return name + counter.intValue();
    }

    /**
     * Convert the name of an element into its xml string representation example :
     * WebApp --> web-app
     * @param name the name of the element to convert
     * @return the xml string representation of an element
     */
    protected String convertUpperCaseToXml(String name) {
        StringBuffer sb = new StringBuffer();
        sb.append(Character.toLowerCase(name.charAt(0)));

        for (int i = 1; i < name.length(); i++) {
            if (Character.isUpperCase(name.charAt(i))) {
                sb.append("-");
            }
            sb.append(Character.toLowerCase(name.charAt(i)));

        }
        return sb.toString();

    }

}