/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 org.apache.ivy.plugins.parser.xml;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.ivy.Ivy;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ExtendsDescriptor;
import org.apache.ivy.core.module.descriptor.InheritableItem;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.plugins.namespace.NameSpaceHelper;
import org.apache.ivy.plugins.namespace.Namespace;
import org.apache.ivy.plugins.parser.ParserSettings;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.plugins.repository.file.FileResource;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.util.Checks;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.XMLHelper;
import org.apache.ivy.util.extendable.ExtendableItemHelper;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Used to update ivy files. Uses ivy file as source and not ModuleDescriptor to preserve as much as
 * possible the original syntax
 */
public final class XmlModuleDescriptorUpdater {
    //CheckStyle:StaticVariableName| OFF    
    //LINE_SEPARATOR is actually a constant, but we have to modify it for the tests
    public static String LINE_SEPARATOR = System.getProperty("line.separator");
    //CheckStyle:StaticVariableName| ON
    
    private XmlModuleDescriptorUpdater() {
    }
    
    /**
     * used to copy a module descriptor xml file (also known as ivy file) and update the revisions
     * of its dependencies, its status and revision
     * 
     * @param srcURL
     *            the url of the source module descriptor file
     * @param destFile
     *            The file to which the updated module descriptor should be output
     */
    public static void update(URL srcURL, File destFile, UpdateOptions options) 
            throws IOException, SAXException {
        if (destFile.getParentFile() != null) {
            destFile.getParentFile().mkdirs();
        }
        OutputStream destStream = new FileOutputStream(destFile);
        try {
            update(srcURL, destStream, options);
        } finally {
            try {
                destStream.close();
            } catch (IOException e) {
                Message.warn("failed to close a stream : " + e.toString());
            }
        }
    }

    public static void update(URL srcURL, OutputStream destFile, UpdateOptions options) 
            throws IOException, SAXException {
        InputStream in = srcURL.openStream();
        try {
            update(srcURL, in, destFile, options);
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                Message.warn("failed to close a stream : " + e.toString());
            }
            try {
                destFile.close();
            } catch (IOException e) {
                Message.warn("failed to close a stream : " + e.toString());
            }
        }

    }

    
    public static void update(InputStream in, Resource res, 
            File destFile, UpdateOptions options) throws IOException, SAXException {
        if (destFile.getParentFile() != null) {
            destFile.getParentFile().mkdirs();
        }
        OutputStream fos = new FileOutputStream(destFile);
        try {
            //TODO: use resource as input stream context?
            URL inputStreamContext = null;
            if (res instanceof URLResource) {
                inputStreamContext = ((URLResource) res).getURL();
            } else if (res instanceof FileResource) {
                inputStreamContext = ((FileResource) res).getFile().toURI().toURL();
            }
            update(inputStreamContext, in, fos, options);
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                Message.warn("failed to close a stream : " + e.toString());
            }
            try {
                fos.close();
            } catch (IOException e) {
                Message.warn("failed to close a stream : " + e.toString());
            }
        }
    }

    private static class UpdaterHandler extends DefaultHandler implements LexicalHandler {

        /** standard attributes of ivy-module/info */
        private static final Collection STD_ATTS = Arrays.asList(new String[] {"organisation",
                "module", "branch", "revision", "status", "publication", "namespace"});

        /** elements that may appear inside ivy-module, in expected order */
        private static final List MODULE_ELEMENTS = Arrays.asList(new String[] {
                "info", "configurations", "publications", "dependencies", "conflicts"
        });
        /** element position of "configurations" inside "ivy-module" */
        private static final int CONFIGURATIONS_POSITION = MODULE_ELEMENTS.indexOf("configurations");
        /** element position of "dependencies" inside "ivy-module" */
        private static final int DEPENDENCIES_POSITION = MODULE_ELEMENTS.indexOf("dependencies");

        /** elements that may appear inside of ivy-module/info */
        private static final Collection INFO_ELEMENTS = Arrays.asList(new String[] {"extends",
                "ivyauthor", "license", "repository", "description"});

        private final ParserSettings settings;

        private final PrintWriter out;

        private final Map resolvedRevisions;
        
        private final Map resolvedBranches;

        private final String status;

        private final String revision;

        private final Date pubdate;

        private final Namespace ns;

        private final boolean replaceInclude;
        
        private final boolean generateRevConstraint;

        private boolean inHeader = true;

        private final List confs;

        private final URL relativePathCtx;
        
        private final UpdateOptions options;

        public UpdaterHandler(URL relativePathCtx, PrintWriter out, final UpdateOptions options) {
            this.options = options;
            this.settings = options.getSettings();
            this.out = out;
            this.resolvedRevisions = options.getResolvedRevisions();
            this.resolvedBranches = options.getResolvedBranches();
            this.status = options.getStatus();
            this.revision = options.getRevision();
            this.pubdate = options.getPubdate();
            this.ns = options.getNamespace();
            this.replaceInclude = options.isReplaceInclude();
            this.generateRevConstraint = options.isGenerateRevConstraint();
            this.relativePathCtx = relativePathCtx;
            if (options.getConfsToExclude() != null) {
                this.confs = Arrays.asList(options.getConfsToExclude());
            } else {
                this.confs = Collections.EMPTY_LIST;
            }
        }

        // never print *ln* cause \n is found in copied characters stream
        // nor do we need do handle indentation, original one is maintained except for attributes

        private String organisation = null;

        // defaultConfMapping of imported configurations, if any
        private String defaultConfMapping = null; 

        // confMappingOverride of imported configurations, if any
        private Boolean confMappingOverride = null; 

        // used to know if the last open tag was empty, to adjust termination 
        // with /> instead of ></qName>
        private String justOpen = null;
        
        //track the size of the left indent, so that inserted elements are formatted
        //like nearby elements.

        //true when we're reading indent whitespace
        private boolean indenting;
        private StringBuffer currentIndent = new StringBuffer();
        private ArrayList indentLevels = new ArrayList(); // ArrayList<String>

        //true if an ivy-module/info/description element has been found in the published descriptor
        private boolean hasDescription = false;
        //true if merged configurations have been written
        private boolean mergedConfigurations = false;
        //true if merged deps have been written
        private boolean mergedDependencies = false;

        // the new value of the defaultconf attribute on the publications tag
        private String newDefaultConf = null;
        
        private Stack context = new Stack();

        private Stack buffers = new Stack();

        private Stack confAttributeBuffers = new Stack();

        public void startElement(String uri, String localName, String qName, Attributes attributes)
                throws SAXException {
            inHeader = false;
            endIndent();
            if (justOpen != null) {
                write(">");
            }

            flushMergedElementsBefore(qName);
            context.push(qName);

            String path = getContext();
            if ("info".equals(qName)) {
                infoStarted(attributes);
            } else if (replaceInclude && "include".equals(qName)
                    && context.contains("configurations")) {
                //TODO, in the case of !replaceInclude, we should still replace the relative path
                //by an absolute path. 
                includeStarted(attributes);
            } else if ("ivy-module/info/extends".equals(path)) {
                startExtends(attributes);
            } else if ("ivy-module/dependencies/dependency".equals(path)) {
                startElementInDependency(attributes);
            } else if ("dependencies".equals(qName)) {
                startDependencies(attributes);
            } else if ("ivy-module/configurations/conf".equals(path)) {
                startElementInConfigurationsConf(qName, attributes);
            } else if ("ivy-module/publications/artifact/conf".equals(path)
                    || "ivy-module/dependencies/dependency/conf".equals(path)
                    || "ivy-module/dependencies/dependency/artifact/conf".equals(path)) {
                buffers.push(new ExtendedBuffer(getContext()));
                ((ExtendedBuffer) confAttributeBuffers.peek()).setDefaultPrint(false);
                String confName = substitute(settings, attributes.getValue("name"));
                if (!confs.contains(confName)) {
                    ((ExtendedBuffer) confAttributeBuffers.peek()).setPrint(true);
                    ((ExtendedBuffer) buffers.peek()).setPrint(true);
                    write("<" + qName);
                    for (int i = 0; i < attributes.getLength(); i++) {
                        write(" " + attributes.getQName(i) + "=\""
                                + substitute(settings, attributes.getValue(i)) + "\"");
                    }
                }
            } else if ("ivy-module/publications/artifact".equals(path)) {
                ExtendedBuffer buffer = new ExtendedBuffer(getContext());
                buffers.push(buffer);
                confAttributeBuffers.push(buffer);
                write("<" + qName);
                buffer.setDefaultPrint(attributes.getValue("conf") == null
                        && ((newDefaultConf == null) || (newDefaultConf.length() > 0)));
                for (int i = 0; i < attributes.getLength(); i++) {
                    String attName = attributes.getQName(i);
                    if ("conf".equals(attName)) {
                        String confName = substitute(settings, attributes.getValue("conf"));
                        String newConf = removeConfigurationsFromList(confName, confs);
                        if (newConf.length() > 0) {
                            write(" " + attributes.getQName(i) + "=\"" + newConf + "\"");
                            ((ExtendedBuffer) buffers.peek()).setPrint(true);
                        }
                    } else {
                        write(" " + attributes.getQName(i) + "=\""
                                + substitute(settings, attributes.getValue(i)) + "\"");
                    }
                }
            } else if ("ivy-module/dependencies/dependency/artifact".equals(path)) {
                ExtendedBuffer buffer = new ExtendedBuffer(getContext());
                buffers.push(buffer);
                confAttributeBuffers.push(buffer);
                write("<" + qName);
                buffer.setDefaultPrint(attributes.getValue("conf") == null);
                for (int i = 0; i < attributes.getLength(); i++) {
                    String attName = attributes.getQName(i);
                    if ("conf".equals(attName)) {
                        String confName = substitute(settings, attributes.getValue("conf"));
                        String newConf = removeConfigurationsFromList(confName, confs);
                        if (newConf.length() > 0) {
                            write(" " + attributes.getQName(i) + "=\"" + newConf + "\"");
                            ((ExtendedBuffer) buffers.peek()).setPrint(true);
                        }
                    } else {
                        write(" " + attributes.getQName(i) + "=\""
                                + substitute(settings, attributes.getValue(i)) + "\"");
                    }
                }
            } else if ("ivy-module/publications".equals(path)) {
                startPublications(attributes);
            } else {
                if (options.isMerge() && path.startsWith("ivy-module/info")) {
                    ModuleDescriptor merged = options.getMergedDescriptor();
                    if (path.equals("ivy-module/info/description")) {
                        //if the descriptor already contains a description, don't bother printing
                        //the merged version.
                        hasDescription = true;
                    } else if (!INFO_ELEMENTS.contains(qName)) {
                        //according to the XSD, we should write description after all of the other
                        //standard <info> elements but before any extended elements.
                        writeInheritedDescription(merged);
                    }
                }

                // copy
                write("<" + qName);
                for (int i = 0; i < attributes.getLength(); i++) {
                    write(" " + attributes.getQName(i) + "=\""
                            + substitute(settings, attributes.getValue(i)) + "\"");
                }
            }
            justOpen = qName;
            // indent.append("\t");
        }

        private void startExtends(Attributes attributes) {
            // in merge mode, comment out extends element
            if (options.isMerge()) {
                write("<!-- ");
            }
            write("<extends");

            String org = substitute(settings, attributes.getValue("organisation"));
            String module = substitute(settings, attributes.getValue("module"));
            ModuleId parentId = new ModuleId(org, module);

            for (int i = 0; i < attributes.getLength(); i++) {
                String name = attributes.getQName(i);
                String value = null;

                if ("revision".equals(name)) {
                    //replace inline revision with resolved parent revision
                    ModuleDescriptor merged = options.getMergedDescriptor();
                    if (merged != null) {
                        ExtendsDescriptor[] parents = merged.getInheritedDescriptors();
                        for (int j = 0; value == null && j < parents.length; ++j) {
                            ModuleRevisionId resolvedId = parents[j].getResolvedParentRevisionId();
                            if (parentId.equals(resolvedId.getModuleId())) {
                                value = resolvedId.getRevision();
                            }
                        }
                    }
                    if (value == null) {
                        value = substitute(settings, attributes.getValue(i));
                    }
                } else if ("organisation".equals(name)) {
                    value = org;
                } else if ("module".equals(name)) {
                    value = module;
                } else {
                    value = substitute(settings, attributes.getValue(i));
                }
                write(" " + name + "=\"" + value + "\"");
            }
        }

        private void startElementInConfigurationsConf(String qName, Attributes attributes) {
            buffers.push(new ExtendedBuffer(getContext()));
            String confName = substitute(settings, attributes.getValue("name"));
            if (!confs.contains(confName)) {
                ((ExtendedBuffer) buffers.peek()).setPrint(true);
                String extend = substitute(settings, attributes.getValue("extends"));
                if (extend != null) {
                    for (StringTokenizer tok = new StringTokenizer(extend, ", "); tok
                            .hasMoreTokens();) {
                        String current = tok.nextToken();
                        if (confs.contains(current)) {
                            throw new IllegalArgumentException(
                                    "Cannot exclude a configuration which is extended.");
                        }
                    }
                }

                write("<" + qName);
                for (int i = 0; i < attributes.getLength(); i++) {
                    write(" " + attributes.getQName(i) + "=\""
                            + substitute(settings, attributes.getValue(i)) + "\"");
                }
            }
        }

        private void startDependencies(Attributes attributes) {
            // copy
            write("<dependencies");
            for (int i = 0; i < attributes.getLength(); i++) {
                String attName = attributes.getQName(i);
                if ("defaultconfmapping".equals(attName)) {
                    String newMapping = removeConfigurationsFromMapping(substitute(settings,
                        attributes.getValue("defaultconfmapping")), confs);
                    if (newMapping.length() > 0) {
                        write(" " + attributes.getQName(i) + "=\"" + newMapping + "\"");
                    }
                } else {
                    write(" " + attributes.getQName(i) + "=\""
                            + substitute(settings, attributes.getValue(i)) + "\"");
                }
            }
            // add default conf mapping if needed
            if (defaultConfMapping != null
                    && attributes.getValue("defaultconfmapping") == null) {
                String newMapping = removeConfigurationsFromMapping(defaultConfMapping, confs);
                if (newMapping.length() > 0) {
                    write(" defaultconfmapping=\"" + newMapping + "\"");
                }
            }
            // add confmappingoverride if needed
            if (confMappingOverride != null
                    && attributes.getValue("confmappingoverride") == null) {
                write(" confmappingoverride=\"" + confMappingOverride.toString() + "\"");
            }
        }
        
        private void startPublications(Attributes attributes) {
            write("<publications");
            for (int i = 0; i < attributes.getLength(); i++) {
                String attName = attributes.getQName(i);
                if ("defaultconf".equals(attName)) {
                    newDefaultConf = removeConfigurationsFromList(substitute(settings,
                        attributes.getValue("defaultconf")), confs);
                    if (newDefaultConf.length() > 0) {
                        write(" " + attributes.getQName(i) + "=\"" + newDefaultConf + "\"");
                    }
                } else {
                    write(" " + attributes.getQName(i) + "=\""
                            + substitute(settings, attributes.getValue(i)) + "\"");
                }
            }
        }

        private void startElementInDependency(Attributes attributes) {
            ExtendedBuffer buffer = new ExtendedBuffer(getContext());
            buffers.push(buffer);
            confAttributeBuffers.push(buffer);
            buffer.setDefaultPrint(attributes.getValue("conf") == null
                || attributes.getValue("conf").trim().length() == 0);
            write("<dependency");
            String org = substitute(settings, attributes.getValue("org"));
            org = org == null ? organisation : org;
            String module = substitute(settings, attributes.getValue("name"));
            String branch = substitute(settings, attributes.getValue("branch"));
            String branchConstraint = substitute(settings, attributes.getValue("branchConstraint"));
            branchConstraint = branchConstraint == null ? branch : branchConstraint;
            
            // look for the branch used in resolved revisions
            if (branch == null) {
                ModuleId mid = ModuleId.newInstance(org, module);
                if (ns != null) {
                    mid = NameSpaceHelper.transform(mid, ns.getToSystemTransformer());
                }
                for (Iterator iter = resolvedRevisions.keySet().iterator(); iter.hasNext();) {
                    ModuleRevisionId mrid = (ModuleRevisionId) iter.next();
                    if (mrid.getModuleId().equals(mid)) {
                        branch = mrid.getBranch();
                        break;
                    }
                }
            }
            
            String revision = substitute(settings, attributes.getValue("rev"));
            String revisionConstraint = substitute(settings, attributes.getValue("revConstraint"));
            Map extraAttributes = ExtendableItemHelper.getExtraAttributes(settings, attributes,
                XmlModuleDescriptorParser.DEPENDENCY_REGULAR_ATTRIBUTES);
            ModuleRevisionId localMrid = ModuleRevisionId.newInstance(org, module, branch,
                revision, extraAttributes);
            ModuleRevisionId systemMrid = ns == null ? localMrid : ns.getToSystemTransformer()
                    .transform(localMrid);
            
            String newBranch = (String) resolvedBranches.get(systemMrid);

            for (int i = 0; i < attributes.getLength(); i++) {
                String attName = attributes.getQName(i);
                if ("rev".equals(attName)) {
                    String rev = (String) resolvedRevisions.get(systemMrid);
                    if (rev != null) {
                        write(" rev=\"" + rev + "\"");
                        if (attributes.getIndex("branchConstraint") == -1 
                                && branchConstraint != null) {
                            write(" branchConstraint=\"" + branchConstraint + "\"");
                        }
                        if (generateRevConstraint && attributes.getIndex("revConstraint") == -1
                                && !rev.equals(systemMrid.getRevision())) {
                            write(" revConstraint=\"" + systemMrid.getRevision() + "\"");
                        }
                    } else {
                        write(" rev=\"" + systemMrid.getRevision() + "\"");
                    }
                } else if ("revConstraint".equals(attName)) {
                    write(" revConstraint=\"" + revisionConstraint + "\"");
                } else if ("org".equals(attName)) {
                    write(" org=\"" + systemMrid.getOrganisation() + "\"");
                } else if ("name".equals(attName)) {
                    write(" name=\"" + systemMrid.getName() + "\"");
                } else if ("branch".equals(attName)) {
                    if(newBranch != null) {                        
                        write(" branch=\"" + newBranch + "\"");
                    }                    
                    else if(!resolvedBranches.containsKey(systemMrid)) {
                        write(" branch=\"" + systemMrid.getBranch() + "\"");
                    }
                    else {
                        // if resolvedBranches contains the systemMrid, but the new branch is null,
                        // the branch attribute will be removed altogether
                    }
                } else if ("branchConstraint".equals(attName)) {
                    write(" branchConstraint=\"" + branchConstraint + "\"");
                } else if ("conf".equals(attName)) {
                    String oldMapping = substitute(settings, attributes.getValue("conf"));
                    if (oldMapping.length() > 0) {
                        String newMapping = removeConfigurationsFromMapping(oldMapping, confs);
                        if (newMapping.length() > 0) {
                            write(" conf=\"" + newMapping + "\"");
                            ((ExtendedBuffer) buffers.peek()).setPrint(true);
                        }
                    }
                } else {
                    write(" " + attName + "=\""
                            + substitute(settings, attributes.getValue(attName)) + "\"");
                }
            }
            
            if(attributes.getIndex("branch") == -1)
            {            
                if (newBranch != null) {
                    // erase an existing branch attribute if its new value is blank
                    if(!newBranch.trim().equals(""))
                        write(" branch=\"" + newBranch + "\"");
                }            
                else if (options.isUpdateBranch() && systemMrid.getBranch() != null) {
                    // this dependency is on a specific branch, we set it explicitly in the updated file
                    write(" branch=\"" + systemMrid.getBranch() + "\"");
                }
            }
        }

        private void includeStarted(Attributes attributes) throws SAXException {
            final ExtendedBuffer buffer = new ExtendedBuffer(getContext());
            buffers.push(buffer);
            try {
                URL url;
                if (settings != null) {
                    url = settings.getRelativeUrlResolver().getURL(relativePathCtx,
                        settings.substitute(attributes.getValue("file")),
                        settings.substitute(attributes.getValue("url")));
                } else {
                    //TODO : settings can be null, but I don't why.  
                    //Check if the next code is correct in that case
                    String fileName = attributes.getValue("file");
                    if (fileName == null) {
                        String urlStr = attributes.getValue("url");
                        url = new URL(urlStr);
                    } else {
                        url = Checks.checkAbsolute(fileName, "settings.include").toURI().toURL();
                    }
                }
                XMLHelper.parse(url, null, new DefaultHandler() {
                    private boolean insideConfigurations = false;

                    private boolean doIndent = false;

                    public void startElement(String uri, String localName, String qName,
                            Attributes attributes) throws SAXException {
                        if ("configurations".equals(qName)) {
                            insideConfigurations = true;
                            String defaultconf = substitute(settings, attributes
                                    .getValue("defaultconfmapping"));
                            if (defaultconf != null) {
                                defaultConfMapping = defaultconf;
                            }
                            String mappingOverride = substitute(settings, attributes
                                    .getValue("confmappingoverride"));
                            if (mappingOverride != null) {
                                confMappingOverride = Boolean.valueOf(mappingOverride);
                            }
                        } else if ("conf".equals(qName) && insideConfigurations) {
                            String confName = substitute(settings, attributes.getValue("name"));
                            if (!confs.contains(confName)) {
                                buffer.setPrint(true);
                                if (doIndent) {
                                    write("/>\n\t\t");
                                }
                                String extend = substitute(settings, 
                                    attributes.getValue("extends"));
                                if (extend != null) {
                                    for (StringTokenizer tok = new StringTokenizer(extend, ", "); 
                                            tok.hasMoreTokens();) {
                                        String current = tok.nextToken();
                                        if (confs.contains(current)) {
                                            throw new IllegalArgumentException("Cannot exclude a " 
                                                    + "configuration which is extended.");
                                        }
                                    }

                                }

                                write("<" + qName);
                                for (int i = 0; i < attributes.getLength(); i++) {
                                    write(" " + attributes.getQName(i) + "=\""
                                            + substitute(settings, attributes.getValue(i))
                                            + "\"");
                                }
                                doIndent = true;
                            }
                        }
                    }

                    public void endElement(String uri, String localName, String name)
                            throws SAXException {
                        if ("configurations".equals(name)) {
                            insideConfigurations = false;
                        }
                    }
                });
            } catch (Exception e) {
                Message.warn("exception occurred while importing configurations: "
                        + e.getMessage());
                throw new SAXException(e);
            }
        }
        
        private void infoStarted(Attributes attributes) {

            String module = substitute(settings, attributes.getValue("module"));
            String rev = null;
            String branch = null;
            String status = null;
            String namespace = null;
            Map/*<String,String>*/ extraAttributes = null;

            if (options.isMerge()) {
                //get attributes from merged descriptor, ignoring raw XML
                ModuleDescriptor merged = options.getMergedDescriptor();
                ModuleRevisionId mergedMrid = merged.getModuleRevisionId();
                organisation = mergedMrid.getOrganisation();
                branch = mergedMrid.getBranch();
                rev = mergedMrid.getRevision();
                status = merged.getStatus();

                //TODO: should namespace be added to ModuleDescriptor interface, so we don't
                // have to do this kind of check?
                if (merged instanceof DefaultModuleDescriptor) {
                    Namespace ns = ((DefaultModuleDescriptor) merged).getNamespace();
                    if (ns != null) {
                        namespace = ns.getName();
                    }
                }
                if (namespace == null) {
                    namespace = attributes.getValue("namespace");
                }

                extraAttributes = merged.getQualifiedExtraAttributes();
            } else {
                //get attributes from raw XML, performing property substitution
                organisation = substitute(settings, attributes.getValue("organisation"));
                rev = substitute(settings, attributes.getValue("revision"));
                branch = substitute(settings, attributes.getValue("branch"));
                status = substitute(settings, attributes.getValue("status"));
                namespace = substitute(settings, attributes.getValue("namespace"));
                extraAttributes = new LinkedHashMap(attributes.getLength());
                for (int i = 0; i < attributes.getLength(); i++) {
                    String qname = attributes.getQName(i);
                    if (!STD_ATTS.contains(qname)) {
                        extraAttributes.put(qname, substitute(settings, attributes.getValue(i)));
                    }
                }
            }
            
            //apply override values provided in options
            if (revision != null) {
                rev = revision;
            }
            if (options.getBranch() != null) {
                branch = options.getBranch();
            }
            if (this.status != null) {
                status = this.status;
            }
            
            //if necessary translate mrid using optional namespace argument
            ModuleRevisionId localMid = ModuleRevisionId.newInstance(organisation, module, branch,
                rev, ExtendableItemHelper.getExtraAttributes(settings, attributes,
                    new String[] {"organisation", "module", "revision", "status", "publication",
                        "namespace"}));
            ModuleRevisionId systemMid = ns == null ? localMid : ns.getToSystemTransformer()
                    .transform(localMid);

            write("<info");
            if (organisation != null) {
                write(" organisation=\"" + XMLHelper.escape(systemMid.getOrganisation()) + "\"");
            }
            write(" module=\"" + XMLHelper.escape(systemMid.getName()) + "\"");
            if (branch != null) {
                write(" branch=\"" + XMLHelper.escape(systemMid.getBranch()) + "\"");
            }
            if (systemMid.getRevision() != null) {
                write(" revision=\"" + XMLHelper.escape(systemMid.getRevision()) + "\"");
            }
            write(" status=\"" + XMLHelper.escape(status) + "\"");
            if (pubdate != null) {
                write(" publication=\"" + Ivy.DATE_FORMAT.format(pubdate) + "\"");
            } else if (attributes.getValue("publication") != null) {
                write(" publication=\""
                        + substitute(settings, attributes.getValue("publication")) + "\"");
            }
            if (namespace != null) {
                write(" namespace=\"" + namespace + "\"");
            }

            for (Iterator extras = extraAttributes.entrySet().iterator(); extras.hasNext();) {
                Map.Entry extra = (Map.Entry) extras.next();
                write(" " + extra.getKey() + "=\"" + extra.getValue() +  "\"");
            }
        }

        private void write(String content) {
            getWriter().print(content);
        }
        
        private PrintWriter getWriter() {
            return buffers.isEmpty() ? out : ((ExtendedBuffer) buffers.peek()).getWriter();
        }

        private String getContext() {
            StringBuffer buf = new StringBuffer();
            for (Iterator iter = context.iterator(); iter.hasNext();) {
                String ctx = (String) iter.next();
                buf.append(ctx).append("/");
            }
            if (buf.length() > 0) {
                buf.setLength(buf.length() - 1);
            }
            return buf.toString();
        }

        private String substitute(ParserSettings ivy, String value) {
            String result = ivy == null ? value : ivy.substitute(value);
            return XMLHelper.escape(result);
        }

        private String removeConfigurationsFromMapping(String mapping, List confsToRemove) {
            StringBuffer newMapping = new StringBuffer();
            String mappingSep = "";
            for (StringTokenizer tokenizer = new StringTokenizer(mapping, ";"); tokenizer
                    .hasMoreTokens();) {
                String current = tokenizer.nextToken();
                String[] ops = current.split("->");
                String[] lhs = ops[0].split(",");
                List confsToWrite = new ArrayList();
                for (int j = 0; j < lhs.length; j++) {
                    if (!confs.contains(lhs[j].trim())) {
                        confsToWrite.add(lhs[j]);
                    }
                }
                if (!confsToWrite.isEmpty()) {
                    newMapping.append(mappingSep);

                    String sep = "";
                    for (Iterator it = confsToWrite.iterator(); it.hasNext();) {
                        newMapping.append(sep);
                        newMapping.append(it.next());
                        sep = ",";
                    }
                    if (ops.length == 2) {
                        newMapping.append("->");
                        newMapping.append(ops[1]);
                    }
                    mappingSep = ";";
                }
            }

            return newMapping.toString();
        }

        private String removeConfigurationsFromList(String list, List confsToRemove) {
            StringBuffer newList = new StringBuffer();
            String listSep = "";
            for (StringTokenizer tokenizer = new StringTokenizer(list, ","); tokenizer
                    .hasMoreTokens();) {
                String current = tokenizer.nextToken();
                if (!confsToRemove.contains(current.trim())) {
                    newList.append(listSep);
                    newList.append(current);
                    listSep = ",";
                }
            }

            return newList.toString();
        }

        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
            characters(ch, start, length);
        }

        public void characters(char[] ch, int start, int length) throws SAXException {
            if (justOpen != null) {
                write(">");
                justOpen = null;
            }
            write(XMLHelper.escape(String.valueOf(ch, start, length)));
            
            //examine characters for current indent level, keeping in mind
            //that our indent might be split across multiple calls to characters()
            for (int i = start, end = start + length; i < end; ++i) {
                char c = ch[i];
                if (c == '\r' || c == '\n') {
                    //newline resets the indent level
                    currentIndent.setLength(0);
                    indenting = true;
                } else if (indenting) {
                    //indent continues until first non-whitespace character
                    if (Character.isWhitespace(c)) {
                        currentIndent.append(c);
                    } else {
                        endIndent();
                    }
                }
            }
        }

        /** record the current indent level for future elements that appear at the same depth */
        private void endIndent() {
            if (indenting) {
                //record the indent at this level.  if we insert any elements at
                //this level, we'll use the same indent.
                setIndent(context.size() - 1, currentIndent.toString());
                indenting = false;
            }
        }

        /**
         * Set the indent for the given depth.  Indents less than the provided depth
         * will be calculated automatically, if they have not already been defined.
         */
        private void setIndent(int level, String indent) {
            fillIndents(level);
            indentLevels.set(level, indent);
        }

        /**
         * Guarantee that indent levels have been calculated up to and including the
         * given depth (starting at 0).
         */
        private void fillIndents(int level) {
            if (indentLevels.isEmpty()) {
                //add a default single-level indent until we see indents in the document
                indentLevels.add("    "); 
            }
            String oneLevel = (String) indentLevels.get(0);
            for (int fill = indentLevels.size(); fill <= level; ++fill) {
                indentLevels.add(indentLevels.get(fill - 1) + oneLevel);
            }
        }

        /** get the whitespace that should precede new elements at the current depth in the document */
        private String getIndent() {
            int level = context.size() - 1;
            fillIndents(level);
            return (String) indentLevels.get(level);
        }

        /**
         * Write XML elements that do not appear in the source descriptor, but have been copied in
         * from a parent module descriptor via &lt;extends&gt; declaration.
         * @param merged child descriptor containing the merged data
         * @param items the list of inherited items to print
         * @param printer a printer that knows how to write the given type of item
         * @param itemName the name of the container element, e.g. "configurations"
         * @param includeContainer if true, include an enclosing element named
         *   <code>itemName</code>. Otherwise just write the inherited items inline,
         *   with a comment indicating where they came from.
         */
        private void writeInheritedItems(ModuleDescriptor merged, 
                InheritableItem[] items, ItemPrinter printer, String itemName,
                boolean includeContainer) {
            //first categorize inherited items by their source module, so that 
            //we can add some useful comments
            PrintWriter out = getWriter();

            Map inheritedItems = collateInheritedItems(merged, items);
            boolean hasItems = !inheritedItems.isEmpty();

            if (hasItems && includeContainer) {
                if (currentIndent.length() == 0) {
                    out.print(getIndent());
                }
                out.print("<" + itemName + ">");
                context.push(itemName);
                justOpen = null;
            }

            for (Iterator parents = inheritedItems.entrySet().iterator(); parents.hasNext();) {
                Map.Entry entry = (Map.Entry) parents.next();
                ModuleRevisionId parent = (ModuleRevisionId) entry.getKey();
                List list = (List) entry.getValue();

                if (justOpen != null) {
                    out.println(">");
                    justOpen = null; //helps endElement() decide how to write close tags
                }
                writeInheritanceComment(itemName, parent);
                for (int c = 0; c < list.size(); ++c) {
                    InheritableItem item = (InheritableItem) list.get(c);
                    out.print(getIndent());
                    printer.print(merged, item, out);
                }
            }

            if (hasItems) {
                if (includeContainer) {
                    context.pop();
                    out.println(getIndent() + "</" + itemName + ">");
                    out.println();
                }
                //restore the prior indent
                out.print(currentIndent);
            }
        }


        private void writeInheritanceComment(String itemDescription, Object parentInfo) {
            PrintWriter out = getWriter();
            out.println();
            out.println(getIndent() + "<!-- " + itemDescription + " inherited from "
                + parentInfo + " -->");
        }

        /**
         * Collect the given list of inherited descriptor items into lists keyed by parent Id.
         * Thus all of the items inherited from parent A can be written together, then all of
         * the items from parent B, and so on.
         * @param merged the merged child descriptor
         * @param items the inherited items to collate
         * @return maps parent ModuleRevisionId to a List of InheritedItems imported from that parent
         */
        private Map/*<ModuleRevisionId,List>*/ collateInheritedItems(ModuleDescriptor merged, 
                InheritableItem[] items) {
            LinkedHashMap/*<ModuleRevisionId,List>*/ inheritedItems = new LinkedHashMap();
            for (int i = 0; i < items.length; ++i) {
                ModuleRevisionId source = items[i].getSourceModule();
                //ignore items that are defined directly in the child descriptor
                if (source != null 
                    && !source.getModuleId().equals(merged.getModuleRevisionId().getModuleId())) {
                    List accum = (List) inheritedItems.get(source);
                    if (accum == null) {
                        accum = new ArrayList();
                        inheritedItems.put(source, accum);
                    }
                    accum.add(items[i]);
                }
            }
            return inheritedItems;
        }

        /**
         * If no info/description element has yet been written, write the description inherited from
         * the parent descriptor, if any.  Calling this method more than once has no affect.
         */
        private void writeInheritedDescription(ModuleDescriptor merged) {
            if (!hasDescription) {
                hasDescription = true;
                String description = merged.getDescription();
                if (description != null) {
                    PrintWriter writer = getWriter();
                    if (justOpen != null) {
                        writer.println(">");
                    }
                    writeInheritanceComment("description", "parent");
                    writer.println(getIndent() + "<description>" + XMLHelper.escape(description) + "</description>");
                    //restore the indent that existed before we wrote the extra elements
                    writer.print(currentIndent);
                    justOpen = null;
                }
            }
        }

        private void writeInheritedConfigurations(ModuleDescriptor merged) {
            if (!mergedConfigurations) {
                mergedConfigurations = true;
                writeInheritedItems(merged, merged.getConfigurations(),
                    ConfigurationPrinter.INSTANCE, "configurations", false);
            }
        }

        private void writeInheritedDependencies(ModuleDescriptor merged) {
            if (!mergedDependencies) {
                mergedDependencies = true;
                writeInheritedItems(merged, merged.getDependencies(),
                    DependencyPrinter.INSTANCE, "dependencies", false);
            }
        }

        /**
         * <p>If publishing in merge mode, guarantee that any merged elements appearing
         * before <code>moduleElement</code> have been written.  This method should
         * be called <i>before</i> we write the start tag of <code>moduleElement</code>.
         * This covers cases where merged elements like "configurations" and "dependencies" appear
         * in the parent descriptor, but are completely missing in the child descriptor.</p>
         *
         * <p>For example, if "moduleElement" is "dependencies", guarantees that "configurations"
         * has been written.  If <code>moduleElement</code> is <code>null</code>, then all
         * missing merged elements will be flushed.</p>
         *
         * @param moduleElement a descriptor element name, for example "configurations" or "info"
         */
        private void flushMergedElementsBefore(String moduleElement) {
            if (options.isMerge() && context.size() == 1 && "ivy-module".equals(context.peek())
                && !(mergedConfigurations && mergedDependencies)) {

                //calculate the position of the element in ivy-module
                int position = moduleElement == null ? MODULE_ELEMENTS.size()
                        : MODULE_ELEMENTS.indexOf(moduleElement);

                ModuleDescriptor merged = options.getMergedDescriptor();

                //see if we should write <configurations>
                if (!mergedConfigurations && position > CONFIGURATIONS_POSITION
                        && merged.getConfigurations().length > 0) {

                    mergedConfigurations = true;
                    writeInheritedItems(merged, merged.getConfigurations(),
                            ConfigurationPrinter.INSTANCE, "configurations", true);

                }
                //see if we should write <dependencies>
                if (!mergedDependencies && position > DEPENDENCIES_POSITION
                    && merged.getDependencies().length > 0) {

                    mergedDependencies = true;
                    writeInheritedItems(merged, merged.getDependencies(),
                            DependencyPrinter.INSTANCE, "dependencies", true);

                }
            }
        }
        private void flushAllMergedElements() {
            flushMergedElementsBefore(null);
        }

        public void endElement(String uri, String localName, String qName) throws SAXException {
            
            String path = getContext();
            if (options.isMerge()) {
                ModuleDescriptor merged = options.getMergedDescriptor();

                if ("ivy-module/info".equals(path)) {
                    //guarantee that inherited description has been written before
                    //info element closes.
                    writeInheritedDescription(merged);
                } else if ("ivy-module/configurations".equals(path)) {
                    //write inherited configurations after all child configurations
                    writeInheritedConfigurations(merged);
                } else if ("ivy-module/dependencies".equals(path)) {
                    //write inherited dependencies after all child dependencies
                    writeInheritedDependencies(merged);
                } else if ("ivy-module".equals(path)) {
                    //write any remaining inherited data before we close the
                    //descriptor.
                    flushAllMergedElements();
                }
            }
            
            if (qName.equals(justOpen)) {
                write("/>");
            } else {
                write("</" + qName + ">");
            }

            if (!buffers.isEmpty()) {
                ExtendedBuffer buffer = (ExtendedBuffer) buffers.peek();
                if (buffer.getContext().equals(path)) {
                    buffers.pop();
                    if (buffer.isPrint()) {
                        write(buffer.toString());
                    }
                }
            }

            if (!confAttributeBuffers.isEmpty()) {
                ExtendedBuffer buffer = (ExtendedBuffer) confAttributeBuffers.peek();
                if (buffer.getContext().equals(path)) {
                    confAttributeBuffers.pop();
                }
            }

            //<extends> element is commented out when in merge mode.
            if (options.isMerge() && "ivy-module/info/extends".equals(path)) {
                write(" -->");
            }

            justOpen = null;
            context.pop();
        }

        public void endDocument() throws SAXException {
            out.print(LINE_SEPARATOR);
            out.flush();
            out.close();
        }
        
        public void processingInstruction(String target, String data) throws SAXException {
            write("<?");
            write(target);
            write(" ");
            write(data);
            write("?>");
            write(LINE_SEPARATOR);
        }

        public void warning(SAXParseException e) throws SAXException {
            throw e;
        }

        public void error(SAXParseException e) throws SAXException {
            throw e;
        }

        public void fatalError(SAXParseException e) throws SAXException {
            throw e;
        }

        public void endCDATA() throws SAXException {
        }

        public void endDTD() throws SAXException {
        }

        public void startCDATA() throws SAXException {
        }

        public void comment(char[] ch, int start, int length) throws SAXException {
            if (justOpen != null) {
                write(">");
                justOpen = null;
            }

            StringBuffer comment = new StringBuffer();
            comment.append(ch, start, length);
            write("<!--");
            write(comment.toString());
            write("-->");

            if (inHeader) {
                write(LINE_SEPARATOR);
            }
        }

        public void endEntity(String name) throws SAXException {
        }

        public void startEntity(String name) throws SAXException {
        }

        public void startDTD(String name, String publicId, String systemId) throws SAXException {
        }

    }

    public static void update(URL inStreamCtx, InputStream inStream,
            OutputStream outStream, final UpdateOptions options) 
            throws IOException, SAXException {
        final PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream, "UTF-8"));
        out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        out.write(LINE_SEPARATOR);
        
        try {
            UpdaterHandler updaterHandler = new UpdaterHandler(inStreamCtx, out, options);
            InputSource inSrc = new InputSource(new BufferedInputStream(inStream));
            if (inStreamCtx != null) {
                inSrc.setSystemId(inStreamCtx.toExternalForm());
            }
            XMLHelper.parse(inSrc, null, updaterHandler, updaterHandler);
        } catch (ParserConfigurationException e) {
            IllegalStateException ise = new IllegalStateException(
                    "impossible to update Ivy files: parser problem");
            ise.initCause(e);
            throw ise;
        }
    }

    private static class ExtendedBuffer {
        private String context = null;

        private Boolean print = null;

        private boolean defaultPrint = false;

        private StringWriter buffer = new StringWriter();
        
        private PrintWriter writer = new PrintWriter(buffer);

        ExtendedBuffer(String context) {
            this.context = context;
        }

        boolean isPrint() {
            if (print == null) {
                return defaultPrint;
            }
            return print.booleanValue();
        }

        void setPrint(boolean print) {
            this.print = Boolean.valueOf(print);
        }

        void setDefaultPrint(boolean print) {
            this.defaultPrint = print;
        }

        PrintWriter getWriter() {
            return writer;
        }

        String getContext() {
            return context;
        }
        
        public String toString() {
            writer.flush();
            return buffer.toString();
        }
    }
    
    /**
     * Prints a descriptor item's XML representation
     */
    protected static interface ItemPrinter {
        /**
         * Print an XML representation of <code>item</code> to <code>out</code>.
         * @param parent the module descriptor containing <code>item</code>
         * @param item subcomponent of the descriptor, for example a {@link DependencyDescriptor} 
         *   or {@link Configuration}
         */
        public void print(ModuleDescriptor parent, Object item, PrintWriter out);
    }
    
    protected static class DependencyPrinter implements ItemPrinter {
        
        public static final DependencyPrinter INSTANCE = new DependencyPrinter();
        
        public void print(ModuleDescriptor parent, Object item, PrintWriter out) {
            XmlModuleDescriptorWriter.printDependency(parent, (DependencyDescriptor) item, out);
        }
    }
    
    protected static class ConfigurationPrinter implements ItemPrinter {
        
        public static final ConfigurationPrinter INSTANCE = new ConfigurationPrinter();
        
        public void print(ModuleDescriptor parent, Object item, PrintWriter out) {
            XmlModuleDescriptorWriter.printConfiguration((Configuration) item, out);
        }
    }
}
