//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.plugin.jfc;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import de.ugoe.cs.util.StringTools;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * corrects older JFC log files which sometimes do not contain correct source specifications for
 * events. It parses the file and adds component specifications to the sources, that do not have
 * them. For each invalid source it checks, if there is another source with the same
 * <code>toString</code> parameter but a complete list of components. If one is found, it is reused
 * as source for the event with the wrong source. If none is found, a new component list is
 * generated. This contains as parent components the components of the source of the previous event.
 * The leaf component is parsed from the <code>toString</code> parameter that is provided with the
 * source specifications. The resulting leaf nodes are not fully correct. They may pretend to
 * be equal although they are not. Furthermore they may resist at a position in the GUI tree where
 * they are not in reality. But more correctness is not achievable based on the
 * <code>toString</code> parameter. 
 * </p>
 * 
 * @version $Revision: $ $Date: 05.09.2012$
 * @author 2012, last modified by $Author: pharms$
 */
public class JFCTraceCorrector  extends DefaultHandler {

    /**
     * <p>
     * the file to write the result into
     * </p>
     */
    private PrintStream outFile;
    
    /**
     * <p>
     * the currently parsed event
     * </p>
     */
    private Event currentEvent;

    /**
     * <p>
     * the currently parsed source of the currently parsed event
     * </p>
     */
    private Source currentSource;

    /**
     * <p>
     * the list of all sources parsed in a file identified through their <code>toString</code>
     * representation
     * </p>
     */
    private Map<String, List<Source>> allSources = new HashMap<String, List<Source>>();

    /**
     * <p>
     * the currently parsed component of the currently parsed source of the currently parsed event
     * </p>
     */
    private Component currentComponent;

    /**
     * <p>
     * the currently parsed session
     * </p>
     */
    private Session currentSession;

    /**
     * <p>
     * corrects the given file and returns the name of the file into which the result was written
     * </p>
     * 
     * @param filename the name of the file to be corrected
     * 
     * @return the name of the file with the corrected logfile
     * 
     * @throws IllegalArgumentException if the filename is null
     */
    public String correctFile(String filename) throws IllegalArgumentException {
        if (filename == null) {
            throw new IllegalArgumentException("filename must not be null");
        }

        return correctFile(new File(filename)).getAbsolutePath();
    }

    /**
     * <p>
     * corrects the given file, stores the result in the second provided file and returns the
     * name of the file into which the result was written
     * </p>
     * 
     * @param filename   the name of the file to be corrected
     * @param resultFile the name of the file into which the corrected log shall be written
     * 
     * @return the name of the file with the corrected logfile
     * 
     * @throws IllegalArgumentException if the filename or resultFile is null
     */
    public String correctFile(String filename, String resultFile) throws IllegalArgumentException {
        if ((filename == null) | (resultFile == null)) {
            throw new IllegalArgumentException("filename and resultFile must not be null");
        }

        return correctFile(new File(filename), new File(resultFile)).getAbsolutePath();
    }

    /**
     * <p>
     * corrects the given file and returns the file into which the result was written. The name
     * of the resulting file is contains the suffix "_corrected" before the dot.
     * </p>
     * 
     * @param file the file to be corrected
     * 
     * @return the file containing the corrected logfile
     * 
     * @throws IllegalArgumentException if the file is null
     */
    public File correctFile(File file) throws IllegalArgumentException {
        if (file == null) {
            throw new IllegalArgumentException("file must not be null");
        }

        int index = file.getName().lastIndexOf('.');
        String fileName =
            file.getName().substring(0, index) + "_corrected" + file.getName().substring(index);

        File resultFile = new File(file.getParentFile(), fileName);

        return correctFile(file, resultFile);
    }

    /**
     * <p>
     * corrects the given file, stores the result in the second provided file and returns the
     * file into which the result was written
     * </p>
     * 
     * @param file       the file to be corrected
     * @param resultFile the file into which the corrected log shall be written
     * 
     * @return the file with the corrected logfile
     * 
     * @throws IllegalArgumentException if the file or resultFile is null or if they are equal
     */
    public File correctFile(File file, File resultFile) throws IllegalArgumentException {
        if ((file == null) || (resultFile == null)) {
            throw new IllegalArgumentException("file and result file must not be null");
        }
        
        if (file.getAbsolutePath().equals(resultFile.getAbsolutePath())) {
            throw new IllegalArgumentException("file and result file must not be equal");
        }
        
        try {
            outFile = new PrintStream
                (new BufferedOutputStream(new FileOutputStream(resultFile)), false, "UTF-8");
            outFile.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
        }
        catch (FileNotFoundException e1) {
            throw new IllegalArgumentException("could not create a corrected file name " +
                                               resultFile + " next to " + file);
        }
        catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("this host does not support UTF-8 encoding");
        }
        

        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setValidating(true);

        SAXParser saxParser = null;
        InputSource inputSource = null;
        try {
            saxParser = spf.newSAXParser();
            inputSource =
                new InputSource(new InputStreamReader(new FileInputStream(file), "UTF-8"));
        }
        catch (UnsupportedEncodingException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return null;
        }
        catch (ParserConfigurationException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return null;
        }
        catch (SAXException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return null;
        }
        catch (FileNotFoundException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return null;
        }
        if (inputSource != null) {
            inputSource.setSystemId("file://" + file.getAbsolutePath());
            try {
                if (saxParser == null) {
                    throw new RuntimeException("SAXParser creation failed");
                }
                saxParser.parse(inputSource, this);
            }
            catch (SAXParseException e) {
                Console.printerrln("Failure parsing file in line " + e.getLineNumber() +
                    ", column " + e.getColumnNumber() + ".");
                Console.logException(e);
                return null;
            }
            catch (SAXException e) {
                Console.printerr("Error parsing file + " + file.getName());
                Console.logException(e);
                return null;
            }
            catch (IOException e) {
                Console.printerr("Error parsing file + " + file.getName());
                Console.logException(e);
                return null;
            }
        }
        
        if (outFile != null) {
            outFile.close();
        }
        
        return resultFile;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
     * java.lang.String, org.xml.sax.Attributes)
     */
    public void startElement(String uri, String localName, String qName, Attributes atts)
        throws SAXException
    {
        if (qName.equals("sessions")) {
            if (currentSession != null) {
                throw new SAXException("nested sessions are not allowed");
            }
            
            currentSession = new Session("sessions");
        }
        else if (qName.equals("newsession")) {
            if (currentSession != null) {
                currentSession.dump(outFile);
            }
            
            currentSession = new Session("newsession");
        }
        else if (qName.equals("event")) {
            if (currentEvent != null) {
                throw new SAXException("nested events are not allowed");
            }
            
            currentEvent = new Event(atts.getValue("id"));
        }
        else if (qName.equals("source")) {
            if (currentSource != null) {
                throw new SAXException("nested sources are not allowed");
            }
            
            currentSource = new Source();
        }
        else if (qName.equals("component")) {
            if (currentComponent != null) {
                throw new SAXException("nested components are not allowed");
            }
            
            currentComponent = new Component();
        }
        else if (qName.equals("param")) {
            if (currentComponent != null) {
                currentComponent.addParameter(atts.getValue("name"), atts.getValue("value"));
            }
            else if (currentSource != null) {
                currentSource.addParameter(atts.getValue("name"), atts.getValue("value"));
            }
            else if (currentEvent != null) {
                currentEvent.addParameter(atts.getValue("name"), atts.getValue("value"));
            }
            else {
                throw new SAXException("parameter occurred at an unexpected place");
            }
        }
        else {
            throw new SAXException("unexpected tag " + qName);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String,
     * java.lang.String)
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        // we do not have to check, if the current objects are null, as the Parser itself will
        // throw an exception, if there is a closing tag for a non existent opening tag. But
        // with a correct opening tag, the member variables will not be null (see startElement())
        if (qName.equals("sessions")) {
            correctSources(currentSession);

            currentSession.dump(outFile);
            currentSession = null;
            allSources.clear();
        }
        else if (qName.equals("newsession")) {
            correctSources(currentSession);
            
            currentSession.dump(outFile);
            currentSession = null;
            allSources.clear();
        }
        else if (qName.equals("event")) {
            currentSession.addEvent(currentEvent);
            currentEvent = null;
        }
        else if (qName.equals("source")) {
            currentEvent.setSource(getUniqueSource(currentSource));
            currentSource = null;
        }
        else if (qName.equals("component")) {
            currentSource.addComponent(currentComponent);
            currentComponent = null;
        }
        else if (!qName.equals("param")) {
            throw new SAXException("unexpected closing tag " + qName);
        }

    }

    /**
     * <p>
     * returns a source object, that is equal to the provided one but that is unique throughout
     * the parsing process. The method may return the provided source, if this is the first
     * occurrence of this source. This method is needed to reduce the amount of source
     * representations that are instantiated during parsing log files. 
     * </p>
     *
     * @param source the source to search for a unique representation
     * 
     * @return the unique representation of the source
     */
    private Source getUniqueSource(Source source) {
        Source existingSource = null;
        
        List<Source> candidates = allSources.get(source.getToStringValue());
        
        if (candidates != null) {
            for (Source candidate : candidates) {
                if (candidate.equals(source)) {
                    existingSource = candidate;
                    break;
                }
            }
        }
        
        if (existingSource == null) {
            if (candidates == null) {
                candidates = new ArrayList<Source>();
                allSources.put(source.getToStringValue(), candidates);
            }
            
            candidates.add(source);
            existingSource = source;
        }
        
        return existingSource;
    }

    /**
     * <p>
     * convenience method to find a source based on its <code>toString</code> parameter value.
     * The method only returns sources, which match the provided <code>toString</code>
     * representation and which have a valid list of components (size greater 0).
     * </p>
     *
     * @param toStringValue the value of the <code>toString</code> parameter the source to find
     *                      must have
     * 
     * @return the source matching the parameter and having a valid list of components or null if
     *         none is found
     */
    private Source findValidSource(String toStringValue) {
        Source existingSource = null;
        
        List<Source> candidates = allSources.get(toStringValue);
        
        if (candidates != null) {
            for (Source candidate : candidates) {
                if ((candidate.getComponents() != null) && (candidate.getComponents().size() > 0))
                {
                    existingSource = candidate;
                    break;
                }
            }
        }
        
        return existingSource;
    }

    /**
     * <p>
     * corrects all wrong sources in the events of the session. For each wrong resource, the
     * {@link #correctEventSource(Event, Source)} method is called.
     * </p>
     *
     * @param session the session of which the events shall be corrected
     */
    private void correctSources(Session session) {
        Source previousSource = null;
        for (Event event : session.getEvents()) {
            if ((event.getSource() == null) || (event.getSource().getComponents() == null) ||
                (event.getSource().getComponents().size() == 0))
            {
                correctEventSource(event, previousSource);
            }
            
            previousSource = event.getSource();
        }
    }

    /**
     * <p>
     * corrects the source of an event. It first searches for a correct source with an equal
     * <code>toString</code> parameter. If there is any, this is reused. Otherwise it creates a
     * new correct source. For this, it copies all parameters of the source of the provided previous
     * event if they are not included in the source already. Furthermore, it adds all components
     * of the source of the previous event. At last, it adds a further component based on the
     * information found in the <code>toString</code> parameter of the source.
     * </p>
     * 
     * @param event          the event of which the source must be corrected
     * @param previousSource the source of the previous event to be potentially reused partially
     */
    private void correctEventSource(Event event, Source previousSource) {
        Source existingSource = null;
        
        if ((event.getSource() != null) && (event.getSource().getToStringValue() != null)) {
            existingSource = findValidSource(event.getSource().getToStringValue());
        }
        
        if (existingSource != null) {
            event.setSource(existingSource);
        }
        else {
            if (previousSource != null) {
                for (Parameter parameterOfPreviousSource : previousSource.getParameters()) {
                    boolean foundParameter = false;
                    for (Parameter parameter : event.getSource().getParameters()) {
                        if (parameter.getName().equals(parameterOfPreviousSource.getName())) {
                            foundParameter = true;
                            break;
                        }
                    }

                    if (!foundParameter) {
                        event.getSource().addParameter(parameterOfPreviousSource);
                    }
                }
    
                for (Component component : previousSource.getComponents()) {
                    if (!(component instanceof ComponentFromToString)) {
                        event.getSource().addComponent(component);
                    }
                }
            }

            event.getSource().addComponent
                (getComponentFromToString(event.getSource().getToStringValue()));
        }
    }

    /**
     * <p>
     * determines a component based on the <code>toString</code> parameter of a source.
     * For this, it parses the parameter value and tries to determine several infos such as the
     * type and the name of it. The resulting components are not always distinguishable. This is,
     * because the <code>toString</code> parameter does not contain sufficient information for
     * correct identification.
     * </p>
     * 
     * @param toStringValue the <code>toString</code> parameter of a source
     * 
     * @return the component parsed from the <code>toString</code> parameter
     */
    private Component getComponentFromToString(String toStringValue) {
        ComponentFromToString component = new ComponentFromToString();
        
        // search for the beginning of the parameters section. Up to this position we find the class
        int start = toStringValue.indexOf('[');
        String clazz = toStringValue.substring(0, start);
        
        // the first parameters are x and y coordinate as well as the size. The size is one
        // parameter, where with and height are separated with an 'x'
        start = toStringValue.indexOf(',', start) + 1;
        int end = toStringValue.indexOf(',', start);
        
        component.setX(Integer.parseInt(toStringValue.substring(start, end)));
        
        start = end + 1;
        end = toStringValue.indexOf(',', start);

        component.setY(Integer.parseInt(toStringValue.substring(start, end)));

        start = end + 1;
        end = toStringValue.indexOf('x', start);

        component.setWidth(Integer.parseInt(toStringValue.substring(start, end)));

        start = end + 1;
        end = toStringValue.indexOf(',', start);

        component.setHeight(Integer.parseInt(toStringValue.substring(start, end)));

        // no start parsing the rest of the parameters and extract those having a key and a 
        // value and whose key is text, defaultIcon, or an alignment
        int intermediate;
        start = end + 1;

        String title = null;
        String icon = null;
        String alignment = null;
        
        do {
            end = toStringValue.indexOf(',', start);
            intermediate = toStringValue.indexOf('[', start);
            
            if ((intermediate >= 0) && (intermediate < end)) {
                // the value of the parameter itself contains brackets. So try to determine the 
                // real end of the parameter
                end = toStringValue.indexOf(']', intermediate);
                end = toStringValue.indexOf(',', end);
            }
            
            if (end < 0) {
                //we reached the end of the stream. So the the end to the "end"
                end = toStringValue.lastIndexOf(']');
            }
            
            intermediate = toStringValue.indexOf('=', start);
            
            if ((intermediate >= 0) && (intermediate < end)) {
                // this is a key value pair, so store the the parameter
                String key = toStringValue.substring(start, intermediate);
                String value = toStringValue.substring(intermediate + 1, end);
                
                if ("text".equals(key)) {
                    title = value;
                }
                else if ("defaultIcon".equals(key)) {
                    icon = value;
                }
                else if ("alignmentX".equals(key) || "alignmentY".equals(key)) {
                    if (alignment == null) {
                        alignment = value;
                    }
                    else {
                        alignment += "/" + value;
                    }
                }
            }
            /*else {
                // this is a simple value, for now simply ignore it
                String key = toStringValue.substring(start, end);
                if (!"invalid".equals(key)) {
                    componentHash += key.hashCode();
                    component.params.add(new String[] { key, "true" });
                }
            }*/
            
            start = end + 1;
        }
        while (start < toStringValue.lastIndexOf(']'));
        
        // finish the component specification by setting the parameters
        if ((title == null) || "".equals(title) || "null".equals(title)) {
            if ((icon == null) || "".equals(icon) || "null".equals(icon)) {
                title = clazz.substring(clazz.lastIndexOf('.') + 1) + "(";
                
                // to be able to distinguish some elements, that usually have no name and icon, try
                // to include some of their specific identifying information in their name.
                if ("org.tigris.gef.presentation.FigTextEditor".equals(clazz) ||
                    "org.argouml.core.propertypanels.ui.UMLTextField".equals(clazz))
                {
                    title += "height " + component.height + ", ";
                }
                else if ("org.argouml.core.propertypanels.ui.UMLLinkedList".equals(clazz) ||
                         "org.argouml.core.propertypanels.ui.LabelledComponent".equals(clazz))
                {
                    title += "position " + component.getX() + "/" + component.getY() + ", ";
                }
                
                title += "alignment " + alignment + ")";
            }
            else {
                // to be able to distinguish some elements, that usually have no name but an icon,
                // try to include some of their specific identifying information in their name.
                if ("org.tigris.toolbar.toolbutton.PopupToolBoxButton".equals(clazz))
                {
                    icon = icon.substring(0, icon.lastIndexOf('@'));
                    title = clazz.substring(clazz.lastIndexOf('.') + 1) + "(position " +
                        component.getX() + ")";
                }
                else {
                    title = icon;
                }
            }
        }
        
        component.addParameter("title", title);
        component.addParameter("class", clazz);
        component.addParameter("icon", ((icon == null) ? "" : icon));
        component.addParameter("index", "-1");
        
        int hashCode = clazz.hashCode() + title.hashCode();
        
        if (hashCode < 0) {
            hashCode = -hashCode;
        }
        
        component.addParameter("hash", Integer.toString(hashCode, 16));

        return component;
    }    

    /**
     * <p>
     * used to dump a list of parameters to the provided print stream
     * </p>
     */
    private static void dumpParams(PrintStream out, List<Parameter> params, String indent) {
        for (Parameter param : params) {
            out.print(indent);
            out.print("<param name=\"");
            out.print(StringTools.xmlEntityReplacement(param.getName()));
            out.print("\" value=\"");
            out.print(StringTools.xmlEntityReplacement(param.getValue()));
            out.println("\" />");
        }
        
    }
    
    /**
     * <p>
     * check if two parameter lists are equal. Thea are equal if the contain the same parameters
     * ignoring their order.
     * </p>
     *
     * @param params1 the first parameter list to be compared
     * @param params2 the second parameter list to be compared
     * 
     * @return true if both lists contain the same parameters, false else.
     */
    private static boolean parametersEqual(List<Parameter> params1, List<Parameter> params2) {
        if (params1 == null) {
            return params2 == null;
        }
        
        if (params2 == null) {
            return false;
        }
        
        if (params1.size() != params2.size()) {
            return false;
        }
        
        boolean found;
        for (Parameter param1 : params1) {
            found = false;
            
            for (Parameter param2 : params2) {
                if (param1.equals(param2)) {
                    found = true;
                    break;
                }
            }
            
            if (!found) {
                return false;
            }
        }
        
        return true;
    }
    
    
    /**
     * <p>
     * used to carry all events of a session and to dump it to the output file
     * </p> 
     */
    private static class Session {
        
        /** */
        private String type;
        
        /** */
        private List<Event> events = new ArrayList<Event>();
        
        /**
         * 
         */
        private Session(String type) {
            if (type == null) {
                throw new IllegalArgumentException("type must not be null");
            }
            
            this.type = type;
        }
        
        /**
         * 
         */
        private void addEvent(Event event) {
            if (event == null) {
                throw new IllegalArgumentException("event must not be null");
            }
            
            events.add(event);
        }
        
        /**
         * @return the events
         */
        private List<Event> getEvents() {
            return Collections.unmodifiableList(events);
        }

        /**
         * 
         */
        private void dump(PrintStream out) {
            if (out == null) {
                throw new IllegalArgumentException("out must not be null");
            }
            
            out.print("<");
            out.print(type);
            out.println(">");

            for (Event event : events) {
                event.dump(out);
            }
            
            out.print("</");
            out.print(type);
            out.println(">");
        }
    }

    /**
     * <p>
     * used to carry all information about an event and to dump it to the output file
     * </p> 
     */
    private static class Event {

        /** */
        private String id;

        /** */
        private List<Parameter> params = new ArrayList<Parameter>();

        /** */
        private Source source;
        
        /**
         * 
         */
        private Event(String id) {
            if (id == null) {
                throw new IllegalArgumentException("id must not be null");
            }
            
            this.id = id;
        }
        
        /**
         * @param source the source to set
         */
        private void setSource(Source source) {
            this.source = source;
        }

        /**
         * @return the source
         */
        private Source getSource() {
            return source;
        }

        /**
         * 
         */
        private void addParameter(String name, String value) {
            if (name == null) {
                throw new IllegalArgumentException("name must not be null");
            }
            
            if (value == null) {
                throw new IllegalArgumentException("value must not be null");
            }
            
            params.add(new Parameter(name, value));
        }
        
        /**
         * 
         */
        private void dump(PrintStream out) {
            if (out == null) {
                throw new IllegalArgumentException("out must not be null");
            }
            
            out.print("<event id=\"");
            out.print(StringTools.xmlEntityReplacement(id));
            out.println("\">");
            
            dumpParams(out, params, " ");
            source.dump(out);
            
            out.println("</event>");
        }
    }

    /**
     * <p>
     * used to carry all information about a source of an event and to dump it to the output file
     * </p> 
     */
    private static class Source {
        
        /** */
        private List<Parameter> params = new ArrayList<Parameter>();
        
        /** */
        private List<Component> components = new ArrayList<Component>();
        
        /** */
        private String toStringValue = null;

        /**
         *
         */
        private String getToStringValue() {
            if (toStringValue == null) {
                for (Parameter param : params) {
                    if (("toString".equals(param.getName())) &&
                        (param.getValue() != null) && (!"".equals(param.getValue())))
                    {
                        toStringValue = param.getValue();
                        break;
                    }
                }
            }
            
            return toStringValue;
        }

        /**
         * 
         */
        private void addParameter(String name, String value) {
            if (name == null) {
                throw new IllegalArgumentException("name must not be null");
            }
            
            if (value == null) {
                throw new IllegalArgumentException("value must not be null");
            }
            
            params.add(new Parameter(name, value));
        }
        
        /**
         * 
         */
        private void addParameter(Parameter parameter) {
            if (parameter == null) {
                throw new IllegalArgumentException("parameter must not be null");
            }
            
            params.add(parameter);
        }
        
        /**
         * @return the params
         */
        private List<Parameter> getParameters() {
            return Collections.unmodifiableList(params);
        }

        /**
         * 
         */
        private void addComponent(Component component) {
            if (component == null) {
                throw new IllegalArgumentException("component must not be null");
            }
            
            components.add(component);
        }
        
        /**
         * @return the components
         */
        private List<Component> getComponents() {
            return Collections.unmodifiableList(components);
        }

        /**
         * 
         */
        private void dump(PrintStream out) {
            if (out == null) {
                throw new IllegalArgumentException("out must not be null");
            }
            
            out.println(" <source>");
            
            dumpParams(out, params, "  ");
            
            for (Component component : components) {
                component.dump(out);
            }
            
            out.println(" </source>");
        }
        
        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            
            if (obj instanceof Source) {
                Source other = (Source) obj;
                
                if (getToStringValue() != null ?
                      !getToStringValue().equals(other.getToStringValue()) :
                          other.getToStringValue() != null)
                {
                    return false;
                }
                
                if (!parametersEqual(params, other.params)) {
                    return false;
                }
                
                if (components == null) {
                    return other.components == null;
                }
                
                if (other.components == null) {
                    return false;
                }
                
                if (components.size() != other.components.size()) {
                    return false;
                }

                for (int i = 0; i < components.size(); i++) {
                    if (!components.get(i).equals(other.components.get(i))) {
                        return false;
                    }
                }
                
                return true;
            }
            
            return false;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            String str = getToStringValue();
            
            if (str != null) {
                return str.hashCode();
            }
            else {
                // ensure that all incomplete sources provide the same hashcode
                return 0;
            }
        }

    }
    
    /**
     * <p>
     * used to carry all information about a component of a source and to dump it to the output file
     * </p> 
     */
    private static class Component {
        
        /** */
        private List<Parameter> params = new ArrayList<Parameter>();
        
        /**
         * 
         */
        protected void addParameter(String name, String value) {
            if (name == null) {
                throw new IllegalArgumentException("name must not be null");
            }
            
            if (value == null) {
                throw new IllegalArgumentException("value must not be null");
            }
            
            params.add(new Parameter(name, value));
        }
        
        /**
         * @return the params
         */
        private List<Parameter> getParameters() {
            return Collections.unmodifiableList(params);
        }

        /**
         * 
         */
        protected void dump(PrintStream out) {
            if (out == null) {
                throw new IllegalArgumentException("out must not be null");
            }
            
            out.println("  <component>");
            dumpParams(out, params, "   ");
            out.println("  </component>");
        }

        /**
         * 
         */
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            
            if (obj instanceof Component) {
                return parametersEqual(params, ((Component) obj).params);
            }
            else {   
               return false;
            }
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            // all components with an equally sized parameter list can be equal. This does not
            // work, if not all component parameters are set yet. But we do not use components
            // in a hash map so we provide an easy implementation
            return params.size();
        }
    }

    /**
     * <p>
     * represents a specific component, which was read from the toString parameter of a source
     * </p> 
     */
    private static class ComponentFromToString extends Component {
        
        /** */
        private int x;
        
        /** */
        private int y;
        
        /** */
        private int width;
        
        /** */
        private int height;
        
        /**
         * @param x the x to set
         */
        private void setX(int x) {
            this.x = x;
        }

        /**
         * @return the x
         */
        private int getX() {
            return x;
        }

       /**
         * @param y the y to set
         */
        private void setY(int y) {
            this.y = y;
        }

        /**
         * @return the y
         */
        private int getY() {
            return y;
        }

        /**
         * @param width the width to set
         */
        private void setWidth(int width) {
            this.width = width;
        }

        /**
         * @param height the height to set
         */
        private void setHeight(int height) {
            this.height = height;
        }

        /**
         * 
         */
        @Override
        protected void dump(PrintStream out) {
            if (out == null) {
                throw new IllegalArgumentException("out must not be null");
            }
            
            out.println("  <component>");
            
            out.print("   ");
            out.print("<param name=\"x\" value=\"");
            out.print(x);
            out.println("\" />");

            out.print("   ");
            out.print("<param name=\"y\" value=\"");
            out.print(y);
            out.println("\" />");

            out.print("   ");
            out.print("<param name=\"width\" value=\"");
            out.print(width);
            out.println("\" />");

            out.print("   ");
            out.print("<param name=\"height\" value=\"");
            out.print(height);
            out.println("\" />");

            dumpParams(out, super.getParameters(), "   ");
            out.println("  </component>");
        }
        
        /**
         * 
         */
        public boolean equals(Object obj) {
            if (!super.equals(obj)) {
                return false;
            }
            
            if (obj instanceof ComponentFromToString) {
                ComponentFromToString other = (ComponentFromToString) obj;
                return (x == other.x) && (y == other.y) &&
                    (width == other.width) && (height == other.height);
            }
            else {   
               return false;
            }
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return super.hashCode() + x + y + width + height;
        }
    }

    /**
     * <p>
     * used to carry all information about a parameter being a key and a value
     * </p> 
     */
    private static class Parameter {
        
        /** */
        private String name;
        
        /** */
        private String value;
        
        /**
         * 
         */
        private Parameter(String name, String value) {
            if (name == null) {
                throw new IllegalArgumentException("name must not be null");
            }
            
            if (value == null) {
                throw new IllegalArgumentException("value must not be null");
            }
            
            this.name = name;
            this.value = value;
        }

        /**
         * @return the name
         */
        private String getName() {
            return name;
        }

        /**
         * @return the value
         */
        private String getValue() {
            return value;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if (obj instanceof Parameter) {
                return
                    (name.equals(((Parameter) obj).name) && value.equals(((Parameter) obj).value));
            }
            else {
                return false;
            }
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return name.hashCode() + value.hashCode();
        }
        
        
    }
        
}
