/*
 * 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.netbeans.modules.apisupport.project;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.apisupport.project.api.ManifestManager;
import org.netbeans.modules.apisupport.project.api.Util;
import org.netbeans.modules.apisupport.project.suite.SuiteProjectType;
import org.netbeans.modules.apisupport.project.universe.ModuleEntry;
import org.netbeans.spi.project.AuxiliaryConfiguration;
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Mutex.ExceptionAction;
import org.openide.util.MutexException;
import org.openide.util.NbBundle.Messages;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.netbeans.modules.apisupport.project.universe.ModuleList;
import org.netbeans.modules.apisupport.project.universe.NbPlatform;
import org.netbeans.modules.apisupport.project.universe.NonexistentModuleEntry;
import org.netbeans.modules.apisupport.project.universe.TestModuleDependency;
import org.openide.filesystems.FileSystem;
import org.openide.util.Mutex;
import org.openide.util.Parameters;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import static org.netbeans.modules.apisupport.project.Bundle.*;

/**
 * Convenience class for managing project's <em>project.xml</em> file. You
 * should explicitly enclose a <em>complete</em> operation within write access
 * to prevent race conditions. Use {@link ProjectManager#saveProject} to apply
 * changes <em>physically</em>.
 */
public final class ProjectXMLManager {

    private static final Logger LOG = Logger.getLogger(ProjectXMLManager.class.getName());

    /** Equal to AntProjectHelper.PROJECT_NS which is package private. */
    // XXX is there a better way? (impact of imposibility to use ProjectGenerator)
    private static final String PROJECT_NS =
            "http://www.netbeans.org/ns/project/1"; // NOI18N
    // elements constants
    private static final String BINARY_ORIGIN = "binary-origin"; // NOI18N
    static final String BUILD_PREREQUISITE = "build-prerequisite"; // NOI18N
    private static final String CLASS_PATH_BINARY_ORIGIN = "binary-origin"; //NOI18N
    private static final String CLASS_PATH_EXTENSION = "class-path-extension"; // NOI18N
    private static final String CLASS_PATH_RUNTIME_PATH = "runtime-relative-path"; //NOI18N
    static final String CODE_NAME_BASE = "code-name-base"; // NOI18N
    static final String COMPILE_DEPENDENCY = "compile-dependency"; // NOI18N
    private static final String DATA = "data"; // NOI18N
    static final String DEPENDENCY = "dependency"; // NOI18N
    private static final String EXTRA_COMPILATION_UNIT = "extra-compilation-unit"; // NOI18N
    private static final String FRIEND = "friend"; // NOI18N
    private static final String FRIEND_PACKAGES = "friend-packages"; // NOI18N
    private static final String IMPLEMENTATION_VERSION = "implementation-version"; // NOI18N
    static final String MODULE_DEPENDENCIES = "module-dependencies"; // NOI18N
    private static final String PACKAGE = "package"; // NOI18N
    private static final String PUBLIC_PACKAGES = "public-packages"; // NOI18N
    private static final String RELEASE_VERSION = "release-version"; // NOI18N
    static final String RUN_DEPENDENCY = "run-dependency"; // NOI18N
    private static final String SPECIFICATION_VERSION = "specification-version"; // NOI18N
    private static final String STANDALONE = "standalone"; // NOI18N
    private static final String SUBPACKAGES = "subpackages"; // NOI18N
    private static final String SUITE_COMPONENT = "suite-component"; // NOI18N
    private static final String TEST_DEPENDENCIES = "test-dependencies"; // NOI18N
    private static final String TEST_TYPE_NAME = "name"; // NOI18N
    private static final String TEST_DEPENDENCY = "test-dependency"; // NOI18N
    private static final String TEST_DEPENDENCY_CNB = "code-name-base"; // NOI18N
    private static final String TEST_DEPENDENCY_RECURSIVE = "recursive"; // NOI18N
    private static final String TEST_DEPENDENCY_COMPILE = "compile-dependency"; // NOI18N
    private static final String TEST_DEPENDENCY_TEST = "test"; // NOI18N
    private static final String TEST_TYPE = "test-type"; //NOI18N
    private static final String[] ORDER = {
        CODE_NAME_BASE,
        SUITE_COMPONENT,
        STANDALONE,
        MODULE_DEPENDENCIES,
        TEST_DEPENDENCIES,
        PUBLIC_PACKAGES,
        FRIEND_PACKAGES,
        CLASS_PATH_EXTENSION,
        EXTRA_COMPILATION_UNIT,
    };
    private final NbModuleProject project;
    private NbPlatform customPlaf;
    private String cnb;
    private SortedSet<ModuleDependency> directDeps;
    private ManifestManager.PackageExport[] publicPackages;
    private Map<String, String> cpExtensions;
    private String[] friends;    // cached confData element for easy access with getConfData
    private Element confData;

    /** Creates a new instance of {@link ProjectXMLManager}. */
    public ProjectXMLManager(final @NonNull NbModuleProject project) {
        Parameters.notNull("project", project);
        this.project = project;
    }

    /**
     * Utility mehtod for getting the {@link ProjectXMLManager instance}
     * associated with a project in the given directory.
     *
     * @throws IOException if the project under a given <code>projectDir</code>
     *         was recognized but could not be loaded (see {@link ProjectManager#findProject}).
     */
    public static ProjectXMLManager getInstance(final File projectDir) throws IOException {
        FileObject dir = FileUtil.toFileObject(projectDir);
        if (dir == null) {
            throw new IOException("no project dir " + projectDir);
        }
        NbModuleProject p = (NbModuleProject) ProjectManager.getDefault().findProject(dir);
        if (p == null) {
            throw new IOException("no project in " + projectDir);
        }
        return new ProjectXMLManager(p);
    }

    public void setModuleType(NbModuleType moduleType) {
        Element _confData = getConfData();
        Document doc = _confData.getOwnerDocument();

        Element standaloneEl = findElement(_confData, ProjectXMLManager.STANDALONE);
        if (standaloneEl != null && moduleType == NbModuleType.STANDALONE) {
            // nothing needs to be done - standalone is already set
            return;
        }

        Element suiteCompEl = findElement(_confData, ProjectXMLManager.SUITE_COMPONENT);
        if (suiteCompEl != null && moduleType == NbModuleType.SUITE_COMPONENT) {
            // nothing needs to be done - suiteCompEl is already set
            return;
        }

        if (suiteCompEl == null && standaloneEl == null && moduleType == NbModuleType.NETBEANS_ORG) {
            // nothing needs to be done - nb.org modules don't have any element
            return;
        }

        // Ok, we get here. So clean up....
        if (suiteCompEl != null) {
            _confData.removeChild(suiteCompEl);
        }
        if (standaloneEl != null) {
            _confData.removeChild(standaloneEl);
        }

        // ....and create element for new module type.
        Element newModuleType = createTypeElement(doc, moduleType);
        if (newModuleType != null) {
            _confData.insertBefore(newModuleType, findModuleDependencies(_confData));
        }
        project.putPrimaryConfigurationData(_confData);
    }

    /**
     * Returns direct module dependencies using default module's platform. See
     * {@link #getDirectDependencies(NbPlatform)} for more details to which
     * this method delegates.
     */
    public SortedSet<ModuleDependency> getDirectDependencies() throws IOException {
        return getDirectDependencies(null);
    }

    /**
     * Returns sorted direct module dependencies using {@link
     * ModuleDependency#CNB_COMPARATOR} allowing to pass a custom platform.
     * Since no two modules with the same code name base may be set as a
     * dependency. Also this is ordering used in the <em>project.xml</em>.
     */
    public SortedSet<ModuleDependency> getDirectDependencies(final NbPlatform customPlaf) throws IOException {
        if (this.customPlaf == customPlaf && this.directDeps != null) {
            return this.directDeps;
        }
        this.customPlaf = customPlaf;
        SortedSet<ModuleDependency> _directDeps = new TreeSet<ModuleDependency>(ModuleDependency.CNB_COMPARATOR);
        Element moduleDependencies = findModuleDependencies(getConfData());
        assert moduleDependencies != null : "Cannot find <module-dependencies> for: " + project;
        File prjDirF = project.getProjectDirectoryFile();
        ModuleList ml;
        if (customPlaf != null) {
            ml = ModuleList.getModuleList(prjDirF, customPlaf.getDestDir());
        } else {
            ml = ModuleList.getModuleList(prjDirF);
        }
        for (Element depEl : XMLUtil.findSubElements(moduleDependencies)) {
            Element cnbEl = findElement(depEl, ProjectXMLManager.CODE_NAME_BASE);
            String _cnb = XMLUtil.findText(cnbEl);
            ModuleDependency depToAdd = getModuleDependency(_cnb, ml, depEl);
            if (depToAdd == null) {
                continue;
            }
            if (!_directDeps.add(depToAdd)) {
                throw new IOException("#175879: corrupted metadata in " + project + "; duplicate dep found: " + depToAdd);
            }
        }
        this.directDeps = Collections.unmodifiableSortedSet(_directDeps);
        return this.directDeps;
    }

    private ModuleDependency getModuleDependency(String cnb, ModuleList ml, Element depEl) {
        ModuleEntry me = ml.getEntry(cnb);
        if (me == null) {
            // XXX might be e.g. shown in nb.errorForreground and "disabled"
            Util.err.log(ErrorManager.WARNING,
                    "Detected dependency on module which cannot be found in " + // NOI18N
                    "the current module's universe (platform, suite): " + cnb); // NOI18N
            me = new NonexistentModuleEntry(cnb);
        }
        String relVer = null;
        String specVer = null;
        boolean implDep = false;
        Element runDepEl = findElement(depEl, ProjectXMLManager.RUN_DEPENDENCY);
        if (runDepEl != null) {
            Element relVerEl = findElement(runDepEl, ProjectXMLManager.RELEASE_VERSION);
            if (relVerEl != null) {
                relVer = XMLUtil.findText(relVerEl);
            }
            Element specVerEl = findElement(runDepEl, ProjectXMLManager.SPECIFICATION_VERSION);
            if (specVerEl != null) {
                specVer = XMLUtil.findText(specVerEl);
            }
            implDep = findElement(runDepEl, ProjectXMLManager.IMPLEMENTATION_VERSION) != null;
        }
        Element compDepEl = findElement(depEl, ProjectXMLManager.COMPILE_DEPENDENCY);
        ModuleDependency newDep = new ModuleDependency(
                me, relVer, specVer, compDepEl != null, implDep);
        newDep.buildPrerequisite = findElement(depEl, ProjectXMLManager.BUILD_PREREQUISITE) != null;
        newDep.runDependency = runDepEl != null;
        return newDep;
    }

    public ModuleDependency getModuleDependency(String cnb) throws IOException {
        return getModuleDependency(cnb, null);
    }

    public ModuleDependency getModuleDependency(String cnb, final NbPlatform customPlaf) throws IOException {
        this.customPlaf = customPlaf;
        Element moduleDependencies = findModuleDependencies(getConfData());
        assert moduleDependencies != null : "Cannot find <module-dependencies> for: " + project;
        File prjDirF = project.getProjectDirectoryFile();
        ModuleList ml;
        if (customPlaf != null) {
            ml = ModuleList.getModuleList(prjDirF, customPlaf.getDestDir());
        } else {
            ml = ModuleList.getModuleList(prjDirF);
        }
        for (Element dep : XMLUtil.findSubElements(moduleDependencies)) {
            Element cnbEl = findElement(dep, ProjectXMLManager.CODE_NAME_BASE);
            String depCnb = XMLUtil.findText(cnbEl);
            if (depCnb.equals(cnb)) {
                return getModuleDependency(cnb, ml, dep);
            }
        }
        return null;
    }

    /** Remove given dependency from the configuration data. */
    public void removeDependency(String cnbToRemove) {
        Element _confData = getConfData();
        Element moduleDependencies = findModuleDependencies(_confData);
        for (Element dep : XMLUtil.findSubElements(moduleDependencies)) {
            Element cnbEl = findElement(dep, ProjectXMLManager.CODE_NAME_BASE);
            String _cnb = XMLUtil.findText(cnbEl);
            if (cnbToRemove.equals(_cnb)) {
                moduleDependencies.removeChild(dep);
            }
        }
        project.putPrimaryConfigurationData(_confData);
        directDeps = null;
    }

    /**
     * Use this for removing more than one dependencies. It's faster then
     * iterating and using <code>removeDependency</code> for every entry.
     */
    public void removeDependencies(Collection<ModuleDependency> depsToDelete) {
        Set<String> cnbsToDelete = new HashSet<String>(depsToDelete.size());
        for (ModuleDependency dep : depsToDelete) {
            cnbsToDelete.add(dep.getModuleEntry().getCodeNameBase());
        }
        removeDependenciesByCNB(cnbsToDelete);
        directDeps = null;
    }

    /**
     * Use this for removing more than one dependencies. It's faster then
     * iterating and using <code>removeDependency</code> for every entry.
     */
    public void removeDependenciesByCNB(Collection<String> cnbsToDelete) {
        Element _confData = getConfData();
        Element moduleDependencies = findModuleDependencies(_confData);
        for (Element dep : XMLUtil.findSubElements(moduleDependencies)) {
            Element cnbEl = findElement(dep, ProjectXMLManager.CODE_NAME_BASE);
            String _cnb = XMLUtil.findText(cnbEl);
            if (cnbsToDelete.remove(_cnb)) {
                moduleDependencies.removeChild(dep);
            }
            if (cnbsToDelete.size() == 0) {
                break; // everything was deleted
            }
        }
        if (cnbsToDelete.size() != 0) {
            Util.err.log(ErrorManager.WARNING,
                    "Some modules weren't deleted: " + cnbsToDelete); // NOI18N
        }
        project.putPrimaryConfigurationData(_confData);
        directDeps = null;
    }

    public void editDependency(ModuleDependency origDep, ModuleDependency newDep) {
        Element _confData = getConfData();
        Element moduleDependencies = findModuleDependencies(_confData);
        List<Element> currentDeps = XMLUtil.findSubElements(moduleDependencies);
        for (Iterator<Element> it = currentDeps.iterator(); it.hasNext();) {
            Element dep = it.next();
            Element cnbEl = findElement(dep, ProjectXMLManager.CODE_NAME_BASE);
            String _cnb = XMLUtil.findText(cnbEl);
            if (_cnb.equals(origDep.getModuleEntry().getCodeNameBase())) {
                moduleDependencies.removeChild(dep);
                Element nextDep = it.hasNext() ? it.next() : null;
                createModuleDependencyElement(moduleDependencies, newDep, nextDep);
                break;
            }
        }
        project.putPrimaryConfigurationData(_confData);
        directDeps = null;
    }

    /**
     * Adds given dependency.
     * Checks for dependency cycles, see {@link #replaceDependencies(java.util.Set)} for details.
     */
    public void addDependency(ModuleDependency md) throws IOException, CyclicDependencyException {
        addDependencies(Collections.singleton(md));
    }

    /**
     * Adds given modules as module-dependencies for the project.
     * Checks for dependency cycles, see {@link #replaceDependencies(java.util.Set)} for details.
     */
    public void addDependencies(final Set<ModuleDependency> toAdd) throws IOException, CyclicDependencyException {
        SortedSet<ModuleDependency> deps = new TreeSet<ModuleDependency>(getDirectDependencies());
        if (deps.addAll(toAdd)) {
            replaceDependencies(deps);
        }
    }

    /**
     * Checks if <code>candidates</code> dependencies introduce dependency cycle.
     * In such case returns localized message about which dependency causes the cycle.
     * @param candidates Module dependencies about to be added. May be empty but not <code>null</code>.
     * @return Localized warning message about introduced dependency cycle,
     * <code>null</code> otherwise.
     */
    @Messages({"# {0} - candidate project name", "# {1} - this project name", "MSG_cyclic_dep=Adding project {0} as dependency to {1} would introduce cyclic dependency! Dependency was not added."})
    public String getDependencyCycleWarning(final Set<ModuleDependency> candidates) {
        for (ModuleDependency md : candidates) {
            File srcLoc = md.getModuleEntry().getSourceLocation();
            if (srcLoc == null) {
                continue;
            }
            FileObject srcLocFO = FileUtil.toFileObject(srcLoc);
            if (srcLocFO == null) {
                continue;
            }
            Project candidate;
            try {
                candidate = ProjectManager.getDefault().findProject(srcLocFO);
            } catch (IOException x) {
                continue;
            }
            if (candidate == null) {
                continue;
            }
            boolean cyclicDep = ProjectUtils.hasSubprojectCycles(project, candidate);
            if (cyclicDep) {
                if (ProjectUtils.hasSubprojectCycles(project, null)) {
                    LOG.log(Level.WARNING, "Starting out with subproject cycles in {0} before even changing them", project);
                    return null;
                } else {
                    String c = ProjectUtils.getInformation(candidate).getDisplayName();
                    String m = ProjectUtils.getInformation(project).getDisplayName();
                    return MSG_cyclic_dep(c, m);
                }
            }
        }
        return null;
    }

    /**
     * Exception thrown when dependency cycle is created when storing
     * new dependencies via project XML manager.
     * {@link #getLocalizedMessage()} return warning message about which project caused the cycle.
     */
    public static class CyclicDependencyException extends Exception {
        private CyclicDependencyException(String message) {
            super(message);
        }
    }

    /**
     * Replaces all original dependencies with the given <code>newDeps</code>.
     *
     * Checks for dependency cycles, throws {@link CyclicDependencyException} if new dependencies
     * introduce dependency cycle and leaves current dependencies untouched.
     */
    public void replaceDependencies(final Set<ModuleDependency> newDeps) throws CyclicDependencyException {
        Set<ModuleDependency> addedDeps = new HashSet<ModuleDependency>(newDeps);
        try {
            SortedSet<ModuleDependency> currentDeps = getDirectDependencies();
            addedDeps.removeAll(currentDeps);
            String warning = getDependencyCycleWarning(addedDeps);
            if (warning != null) {
                throw new CyclicDependencyException(warning);
            }
        } catch (IOException x) { // getDirectDependencies
            LOG.log(Level.INFO, null, x); // and skip check
        }

        Element _confData = getConfData();
        Document doc = _confData.getOwnerDocument();
        Element moduleDependencies = findModuleDependencies(_confData);
        _confData.removeChild(moduleDependencies);
        moduleDependencies = createModuleElement(doc, ProjectXMLManager.MODULE_DEPENDENCIES);
        XMLUtil.appendChildElement(_confData, moduleDependencies, ORDER);
        SortedSet<ModuleDependency> sortedDeps = new TreeSet<ModuleDependency>(newDeps);
        for (ModuleDependency md : sortedDeps) {
            createModuleDependencyElement(moduleDependencies, md, null);
        }
        project.putPrimaryConfigurationData(_confData);
        this.directDeps = sortedDeps;
    }

    public void removeClassPathExtensions() {
        Element _confData = getConfData();
        NodeList nl = _confData.getElementsByTagNameNS(NbModuleProject.NAMESPACE_SHARED,
                ProjectXMLManager.CLASS_PATH_EXTENSION);
        int len = nl.getLength();
        for (int i = 0; i < len; i++) {
            _confData.removeChild(nl.item(0));
        }
        cpExtensions = Collections.emptyMap();
        project.putPrimaryConfigurationData(_confData);
    }

    /**
     * Removes test dependency under type <code>testType</code>, indentified
     * by <code>cnbToRemove</code>. Does not remove whole test type even if
     * removed test dependency was the last one.
     */
    public boolean removeTestDependency(String testType, String cnbToRemove) {
        boolean wasRemoved = false;
        Element _confData = getConfData();
        Element testModuleDependenciesEl = findTestDependenciesElement(_confData);
        Element testTypeRemoveEl = null;
        for (Element type : XMLUtil.findSubElements(testModuleDependenciesEl)) {
            Element nameEl = findElement(type, TEST_TYPE_NAME);
            String nameOfType = XMLUtil.findText(nameEl);
            if (testType.equals(nameOfType)) {
                testTypeRemoveEl = type;
            }
        }
        //found such a test type
        if (testTypeRemoveEl != null) {
            for (Element el : XMLUtil.findSubElements(testTypeRemoveEl)) {
                Element cnbEl = findElement(el, TEST_DEPENDENCY_CNB);
                if (cnbEl == null) {
                    continue;   //name node, continue
                }
                String _cnb = XMLUtil.findText(cnbEl);
                if (cnbToRemove.equals(_cnb)) {
                    // found test dependency with desired CNB
                    testTypeRemoveEl.removeChild(el);
                    wasRemoved = true;
                    project.putPrimaryConfigurationData(_confData);
                }
            }
        }
        return wasRemoved;
    }

    /**
     * Adds new test dependency to <code>project.xml</code>. Currently only two test types are
     * supported - <code>UNIT</code> and <code>QA_FUNCTIONAL</code>. Test dependencies under 
     * test types are sorted by CNB.
     */
    public boolean addTestDependency(final String testType, final TestModuleDependency newTestDep) throws IOException {
        final String UNIT = TestModuleDependency.UNIT;
        final String QA_FUNCTIONAL = TestModuleDependency.QA_FUNCTIONAL;
        assert (UNIT.equals(testType) || QA_FUNCTIONAL.equals(testType)) : "Current impl.supports only " + QA_FUNCTIONAL +
                " or " + UNIT + " tests"; // NOI18N

        final ExceptionAction<Boolean> action = new Mutex.ExceptionAction<Boolean>() {
            public Boolean run() throws Exception {
                File projectDir = FileUtil.toFile(project.getProjectDirectory());
                ModuleList ml = ModuleList.getModuleList(projectDir);
                Map<String, Set<TestModuleDependency>> map = new HashMap<String, Set<TestModuleDependency>>(getTestDependencies(ml));
                Set<TestModuleDependency> testDependenciesSet = map.get(testType);
                if (testDependenciesSet == null) {
                    testDependenciesSet = new TreeSet<TestModuleDependency>();
                    map.put(testType, testDependenciesSet);
                } else {
                    // XXX necessary to clone?
                    testDependenciesSet = new TreeSet<TestModuleDependency>(testDependenciesSet);
                }
                if (!testDependenciesSet.add(newTestDep)) {
                    return false; //nothing new to add, dep is already there, finished
                }
                final Element confData = getConfData();
                final Document doc = confData.getOwnerDocument();
                Element testModuleDependenciesEl = findTestDependenciesElement(confData);
                if (testModuleDependenciesEl == null) {      // test dependencies element does not exist, create it
                    testModuleDependenciesEl = createModuleElement(doc, TEST_DEPENDENCIES);
                    XMLUtil.appendChildElement(confData, testModuleDependenciesEl, ORDER);
                }
                Element testTypeEl = null;
                //iterate through test types to determine if testType exist
                for (Element tt : XMLUtil.findSubElements(testModuleDependenciesEl)) {
                    Node nameNode = findElement(tt, "name"); // NOI18N
                    assert nameNode != null : "should be some child with name";
                    //Node nameNode = tt.getFirstChild();
                    //nameNode.getNodeName()
                    assert (TEST_TYPE_NAME.equals(nameNode.getLocalName())) : "name node should be first child, but was:" + nameNode.getLocalName() +
                    "or" + nameNode.getNodeName(); //NOI18N
                    //equals
                    if (nameNode.getTextContent().equals(testType)) {
                        testTypeEl = tt;
                    }
                }

                // #142594: silently switch to /3 schema
                final Element ttEl = testTypeEl;
                final Element tmdEl = testModuleDependenciesEl;
                final Set<TestModuleDependency> tdSet = testDependenciesSet;

                AuxiliaryConfiguration auxConf = project.getHelper().createAuxiliaryConfiguration();
                auxConf.removeConfigurationFragment(NbModuleProject.NAME_SHARED, NbModuleProject.NAMESPACE_SHARED_2, true);

                //? new or existing test type?
                if (ttEl == null) {
                    //this test type, does not exist, create it, and add new test dependency
                    Element newTestTypeEl = createNewTestTypeElement(doc, testType);
                    tmdEl.appendChild(newTestTypeEl);
                    createTestModuleDependencyElement(newTestTypeEl, newTestDep);
                    project.putPrimaryConfigurationData(confData);
                } else {
                    //testtype exists, refresh it
                    Node beforeWhat = ttEl.getNextSibling();
                    tmdEl.removeChild(ttEl);
                    Element refreshedTestTypeEl = createNewTestTypeElement(doc, testType);
                    if (beforeWhat == null) {
                        tmdEl.appendChild(refreshedTestTypeEl);
                    } else {
                        tmdEl.insertBefore(refreshedTestTypeEl, beforeWhat);
                    }
                    for (TestModuleDependency tmd : tdSet) {
                        createTestModuleDependencyElement(refreshedTestTypeEl, tmd);
                        project.putPrimaryConfigurationData(confData);
                    }
                }
                return true;
            }
        };

        final AtomicBoolean result = new AtomicBoolean();
        project.getProjectDirectory().getFileSystem().runAtomicAction(new FileSystem.AtomicAction() {
            public void run() throws IOException {
                try {
                    project.setRunInAtomicAction(true);
                    result.set(ProjectManager.mutex().writeAccess(action));
                } catch (MutexException ex) {
                    throw (IOException) ex.getCause();
                } finally {
                    project.setRunInAtomicAction(false);
                }
            }
        });
        return result.get();
    }

    private Element createNewTestTypeElement(Document doc, String testTypeName) {
        Element newTestTypeEl = createModuleElement(doc, TEST_TYPE);
        Element nameOfTestTypeEl = createModuleElement(doc, TEST_TYPE_NAME, testTypeName);
        newTestTypeEl.appendChild(nameOfTestTypeEl);
        return newTestTypeEl;
    }

    private void createTestModuleDependencyElement(Element testTypeElement, TestModuleDependency tmd) {
        Document doc = testTypeElement.getOwnerDocument();
        Element tde = createModuleElement(doc, TEST_DEPENDENCY);
        testTypeElement.appendChild(tde);
        tde.appendChild(createModuleElement(doc, TEST_DEPENDENCY_CNB, tmd.getModule().getCodeNameBase()));
        if (tmd.isRecursive()) {
            tde.appendChild(createModuleElement(doc, TEST_DEPENDENCY_RECURSIVE));
        }
        if (tmd.isCompile()) {
            tde.appendChild(createModuleElement(doc, TEST_DEPENDENCY_COMPILE));
        }
        if (tmd.isTest()) {
            tde.appendChild(createModuleElement(doc, TEST_DEPENDENCY_TEST));
        }
    }

    /**
     * Gives a map from test type (e.g. <em>unit</em> or <em>qa-functional</em>)
     * to the set of {@link TestModuleDependency dependencies} belonging to it.
     */
    public @NonNull Map<String, Set<TestModuleDependency>> getTestDependencies(final ModuleList ml) {
        Element testDepsEl = findTestDependenciesElement(getConfData());

        Map<String, Set<TestModuleDependency>> testDeps = new HashMap<String, Set<TestModuleDependency>>();

        if (testDepsEl != null) {
            for (Element typeEl : XMLUtil.findSubElements(testDepsEl)) {
                Element testTypeEl = findElement(typeEl, TEST_TYPE_NAME);
                String testType = null;
                if (testTypeEl != null) {
                    testType = XMLUtil.findText(testTypeEl);
                }
                if (testType == null) {
                    testType = TestModuleDependency.UNIT; // default variant
                }
                Set<TestModuleDependency> directTestDeps = new TreeSet<TestModuleDependency>();
                for (Element depEl : XMLUtil.findSubElements(typeEl)) {
                    if (depEl.getTagName().equals(TEST_DEPENDENCY)) {
                        // parse test dep
                        Element cnbEl = findElement(depEl, TEST_DEPENDENCY_CNB);
                        boolean test = findElement(depEl, TEST_DEPENDENCY_TEST) != null;
                        String _cnb = null;
                        if (cnbEl != null) {
                            _cnb = XMLUtil.findText(cnbEl);
                        }
                        boolean recursive = findElement(depEl, TEST_DEPENDENCY_RECURSIVE) != null;
                        boolean compile = findElement(depEl, TEST_DEPENDENCY_COMPILE) != null;
                        if (_cnb != null) {
                            ModuleEntry me = ml.getEntry(_cnb);
                            if (me == null) {
                                me = new NonexistentModuleEntry(_cnb);
                            }
                                TestModuleDependency tmd = new TestModuleDependency(me, test, recursive, compile);
                                if (!directTestDeps.add(tmd)) {
                                    // testdependency already exists
                                    String path = project.getPathWithinNetBeansOrg();
                                    if (path == null) {
                                        path = project.getProjectDirectoryFile().getAbsolutePath();
                                    }
                                    String msg = "Invalid project.xml (" + path + "); testdependency " // NOI18N
                                            + tmd.getModule().getCodeNameBase() + " is duplicated!"; // NOI18N
                                    Util.err.log(ErrorManager.WARNING, msg);
                                }
                        }
                    }
                }
                testDeps.put(testType, directTestDeps);
            }
        }
        return testDeps;
    }

    /**
     * Replace existing classpath extensions with new values.
     * @param newValues &lt;key=runtime-path(String), value=binary-path(String)&gt;
     */
    public void replaceClassPathExtensions(final Map<String, String> newValues) {
        removeClassPathExtensions();
        if (newValues != null && newValues.size() > 0) {
            Element _confData = getConfData();
            Document doc = _confData.getOwnerDocument();
            for (Map.Entry<String,String> entry : newValues.entrySet()) {
                Element cpel = createModuleElement(doc, ProjectXMLManager.CLASS_PATH_EXTENSION);
                Element runtime = createModuleElement(doc, ProjectXMLManager.CLASS_PATH_RUNTIME_PATH, entry.getKey());
                cpel.appendChild(runtime);
                String binaryPath = entry.getValue();
                if (binaryPath != null) {
                    Element binary = createModuleElement(doc, ProjectXMLManager.CLASS_PATH_BINARY_ORIGIN, binaryPath);
                    cpel.appendChild(binary);
                }
                _confData.appendChild(cpel);
            }
            cpExtensions = new HashMap<String, String>(newValues);
            project.putPrimaryConfigurationData(_confData);
        }
    }

    /**
     * Replaces all original public packages with the given
     * <code>newPackages</code>. Also removes friend packages if there are any
     * since those two mutually exclusive. packages ending with .* will be translated to <subpackages> element
     */
    public void replacePublicPackages(Set<String> newPackages) {
        removePublicAndFriends();
        Element _confData = getConfData();
        Document doc = _confData.getOwnerDocument();
        Element publicPackagesEl = createModuleElement(doc, ProjectXMLManager.PUBLIC_PACKAGES);

        insertPublicOrFriend(publicPackagesEl);

        for (String pkg : newPackages) {
            if (pkg.endsWith(".*")) {
                publicPackagesEl.appendChild(
                    createModuleElement(doc, ProjectXMLManager.SUBPACKAGES, pkg.substring(0, pkg.length() - ".*".length())));
            } else {
                publicPackagesEl.appendChild(
                    createModuleElement(doc, ProjectXMLManager.PACKAGE, pkg));
            }
        }
        project.putPrimaryConfigurationData(_confData);
        publicPackages = null; // XXX cleaner would be to listen on changes in helper
    }

    /** Position public-packages or friend-packages according to XSD. */
    private void insertPublicOrFriend(Element packagesEl) {
        XMLUtil.appendChildElement(getConfData(), packagesEl, ORDER);
    }

    /**
     * Replaces all original friends with the given <code>friends</code> with
     * <code>packagesToExpose</code> as exposed packages to those friends. Also
     * removes public packages if there are any since those two are mutually
     * exclusive. packages ending with .* will be translated to <subpackages> element
     */
    public void replaceFriends(Set<String> friends, Set<String> packagesToExpose) {
        removePublicAndFriends();
        Element _confData = getConfData();
        Document doc = _confData.getOwnerDocument();
        Element friendPackages = createModuleElement(doc, ProjectXMLManager.FRIEND_PACKAGES);
        insertPublicOrFriend(friendPackages);

        for (String friend : friends) {
                friendPackages.appendChild(
                    createModuleElement(doc, ProjectXMLManager.FRIEND, friend));
        }
        for (String pkg : packagesToExpose) {
            if (pkg.endsWith(".*")) {
                friendPackages.appendChild(
                    createModuleElement(doc, ProjectXMLManager.SUBPACKAGES, pkg.substring(0, pkg.length() - ".*".length())));
            } else {
                friendPackages.appendChild(
                    createModuleElement(doc, ProjectXMLManager.PACKAGE, pkg));
            }
        }
        project.putPrimaryConfigurationData(_confData);
        publicPackages = null;
    }

    /**
     * Returns an array of {@link ManifestManager.PackageExport}s of all
     * exposed public packages. Method considers both <em>package</em> and
     * <em>subpackages</em> elements with the recursivity flag set
     * appropriately for returned entries.
     *
     * @return array of {@link ManifestManager.PackageExport}. May be empty but
     *         not <code>null</code>.
     */
    public ManifestManager.PackageExport[] getPublicPackages() {
        if (publicPackages == null) {
            publicPackages = ProjectXMLManager.findPublicPackages(getConfData());
        }
        return publicPackages;
    }

    /** Returns all friends or <code>null</code> if there are none. */
    public String[] getFriends() {
        if (friends == null) {
            friends = ProjectXMLManager.findFriends(getConfData());
        }
        return friends;
    }

    /**
     * Returns paths of all libraries bundled within a project this
     * <em>manager</em> manage. So the result should be an array of
     * <code>String</code>s each representing a relative path to the project's
     * external library (jar/zip).
     * @return an array of strings (may be empty)
     */
    public String[] getBinaryOrigins() {
        Set<String> origins = new LinkedHashSet<String>(getClassPathExtensions().values());
        origins.remove(null);
        return origins.toArray(new String[origins.size()]);
    }

    /**
     * Returns existing classpath extensions mapping.
     * Returned map is unmodifiable.
     * @return classpath extensions map &lt;key=runtime-path(String), value=binary-path(String or null)&gt;
     */
    public Map<String, String> getClassPathExtensions() {
        if (cpExtensions != null) {
            return Collections.unmodifiableMap(cpExtensions);
        }
        Map<String, String> cps = new HashMap<String, String>();
        for (Element cpExtEl : XMLUtil.findSubElements(getConfData())) {
            if (CLASS_PATH_EXTENSION.equals(cpExtEl.getTagName())) {
                Element binOrigEl = findElement(cpExtEl, BINARY_ORIGIN);
                Element runtimePathEl = findElement(cpExtEl, CLASS_PATH_RUNTIME_PATH);
                cps.put(XMLUtil.findText(runtimePathEl), binOrigEl != null ? XMLUtil.findText(binOrigEl) : null);
            }
        }
        return Collections.unmodifiableMap(cpExtensions = cps);
    }

    /** Returns code-name-base. */
    public String getCodeNameBase() {
        if (cnb == null) {
            Element cnbEl = findElement(getConfData(), ProjectXMLManager.CODE_NAME_BASE);
            cnb = XMLUtil.findText(cnbEl);
        }
        return cnb;
    }

    /** Package-private for unit tests only. */
    static void createModuleDependencyElement(
            Element moduleDependencies, ModuleDependency md, Element nextSibling) {

        Document doc = moduleDependencies.getOwnerDocument();
        Element modDepEl = createModuleElement(doc, ProjectXMLManager.DEPENDENCY);
        moduleDependencies.insertBefore(modDepEl, nextSibling);

        modDepEl.appendChild(createModuleElement(doc, ProjectXMLManager.CODE_NAME_BASE,
                md.getModuleEntry().getCodeNameBase()));
        if (md.buildPrerequisite) {
            modDepEl.appendChild(createModuleElement(doc, ProjectXMLManager.BUILD_PREREQUISITE));
        }
        if (md.hasCompileDependency()) {
            modDepEl.appendChild(createModuleElement(doc, ProjectXMLManager.COMPILE_DEPENDENCY));
        }
        if (!md.runDependency) {
            return;
        }

        Element runDepEl = createModuleElement(doc, ProjectXMLManager.RUN_DEPENDENCY);
        modDepEl.appendChild(runDepEl);

        String rv = md.getReleaseVersion();
        if (rv != null && !rv.trim().equals("")) {
            runDepEl.appendChild(createModuleElement(
                    doc, ProjectXMLManager.RELEASE_VERSION, rv));
        }
        if (md.hasImplementationDependency()) {
            runDepEl.appendChild(createModuleElement(
                    doc, ProjectXMLManager.IMPLEMENTATION_VERSION));
        } else {
            String sv = md.getSpecificationVersion();
            if (sv != null && !"".equals(sv)) { // NOI18N
                runDepEl.appendChild(createModuleElement(
                        doc, ProjectXMLManager.SPECIFICATION_VERSION, sv));
            }
        }
    }

    /** Removes public-packages and friend-packages elements. */
    private void removePublicAndFriends() {
        Element friendPackages = findFriendsElement(getConfData());
        if (friendPackages != null) {
            getConfData().removeChild(friendPackages);
        }
        Element _publicPackages = findPublicPackagesElement(getConfData());
        if (_publicPackages != null) {
            getConfData().removeChild(_publicPackages);
        }
    }

    private static Element findElement(Element parentEl, String elementName) {
        return XMLUtil.findElement(parentEl, elementName, NbModuleProject.NAMESPACE_SHARED);
    }

    /** Package-private for unit tests only. */
    static Element findModuleDependencies(Element parentEl) {
        return findElement(parentEl, ProjectXMLManager.MODULE_DEPENDENCIES);
    }

    private static Element findTestDependenciesElement(Element parentEl) {
        return findElement(parentEl, ProjectXMLManager.TEST_DEPENDENCIES);
    }

    private static Element findPublicPackagesElement(Element parentEl) {
        return findElement(parentEl, ProjectXMLManager.PUBLIC_PACKAGES);
    }

    private static Element findFriendsElement(Element parentEl) {
        return findElement(parentEl, ProjectXMLManager.FRIEND_PACKAGES);
    }

    private static Element createModuleElement(Document doc, String name) {
        return doc.createElementNS(NbModuleProject.NAMESPACE_SHARED, name);
    }

    private static Element createModuleElement(Document doc, String name, String innerText) {
        Element el = createModuleElement(doc, name);
        el.appendChild(doc.createTextNode(innerText));
        return el;
    }

    private static Element createSuiteElement(Document doc, String name) {
        return doc.createElementNS(SuiteProjectType.NAMESPACE_SHARED, name);
    }

    private static Element createSuiteElement(Document doc, String name, String innerText) {
        Element el = createSuiteElement(doc, name);
        el.appendChild(doc.createTextNode(innerText));
        return el;
    }

    /**
     * Find packages in public-packages or friend-packages section. Method
     * considers both <em>package</em> and <em>subpackages</em> elements with
     * the recursivity flag set appropriately for returned entries.
     */
    private static Set<ManifestManager.PackageExport> findAllPackages(Element parent) {
        Set<ManifestManager.PackageExport> packages = new HashSet<ManifestManager.PackageExport>();
        try {
            for (Element pkgEl : XMLUtil.findSubElements(parent)) {
                if (PACKAGE.equals(pkgEl.getTagName())) {
                    packages.add(new ManifestManager.PackageExport(XMLUtil.findText(pkgEl), false));
                } else if (SUBPACKAGES.equals(pkgEl.getTagName())) {
                    packages.add(new ManifestManager.PackageExport(XMLUtil.findText(pkgEl), true));
                }
            }
        } catch(IllegalArgumentException e) {
            LOG.log(Level.WARNING, "Error getting subelements, malformed xml");
            packages = new HashSet<ManifestManager.PackageExport>();
        }
        return packages;
    }

    /**
     * Utility method for finding public packages. Method considers both
     * <em>package</em> and <em>subpackages</em> elements with the recursivity
     * flag set appropriately for returned entries.
     *
     * @return array of {@link ManifestManager.PackageExport}. May be empty but
     *         not <code>null</code>.
     */
    public static ManifestManager.PackageExport[] findPublicPackages(final Element confData) {
        Element ppEl = findPublicPackagesElement(confData);
        Set<ManifestManager.PackageExport> pps = new HashSet<ManifestManager.PackageExport>();
        if (ppEl != null) {
            pps.addAll(findAllPackages(ppEl));
        }
        ppEl = findFriendsElement(confData);
        if (ppEl != null) {
            pps.addAll(findAllPackages(ppEl));
        }
        return pps.isEmpty() ? ManifestManager.EMPTY_EXPORTED_PACKAGES : pps.toArray(new ManifestManager.PackageExport[pps.size()]);
    }

    /** Utility method for finding friend. */
    public static String[] findFriends(final Element confData) {
        Element friendsEl = findFriendsElement(confData);
        if (friendsEl != null) {
            Set<String> friends = new TreeSet<String>();
            for (Element friendEl : XMLUtil.findSubElements(friendsEl)) {
                if (FRIEND.equals(friendEl.getTagName())) {
                    friends.add(XMLUtil.findText(friendEl));
                }
            }
            return friends.toArray(new String[friends.size()]);
        }
        return null;
    }

    /**
     * Generates a basic <em>project.xml</em> templates into the given
     * <code>projectXml</code> for <em>standalone</em> or <em>module in
     * suite</em> module.
     */
    static void generateEmptyModuleTemplate(FileObject projectXml, String cnb,
            NbModuleType moduleType, String... compileDepsCnbs) throws IOException {

        Document prjDoc = XMLUtil.createDocument("project", PROJECT_NS, null, null); // NOI18N

        // generate general project elements
        Element typeEl = prjDoc.createElementNS(PROJECT_NS, "type"); // NOI18N
        typeEl.appendChild(prjDoc.createTextNode(NbModuleProject.TYPE));
        prjDoc.getDocumentElement().appendChild(typeEl);
        Element confEl = prjDoc.createElementNS(PROJECT_NS, "configuration"); // NOI18N
        prjDoc.getDocumentElement().appendChild(confEl);

        // generate NB Module project type specific elements
        Element dataEl = createModuleElement(confEl.getOwnerDocument(), DATA);
        confEl.appendChild(dataEl);
        Document dataDoc = dataEl.getOwnerDocument();
        dataEl.appendChild(createModuleElement(dataDoc, CODE_NAME_BASE, cnb));
        Element moduleTypeEl = createTypeElement(dataDoc, moduleType);
        if (moduleTypeEl != null) {
            dataEl.appendChild(moduleTypeEl);
        }
        final Element deps = createModuleElement(dataDoc, MODULE_DEPENDENCIES);
        for (String depCnb : compileDepsCnbs) {
            Element modDepEl = createModuleElement(dataDoc, ProjectXMLManager.DEPENDENCY);
            modDepEl.appendChild(createModuleElement(dataDoc, ProjectXMLManager.CODE_NAME_BASE, depCnb));
            modDepEl.appendChild(createModuleElement(dataDoc, ProjectXMLManager.BUILD_PREREQUISITE));
            modDepEl.appendChild(createModuleElement(dataDoc, ProjectXMLManager.COMPILE_DEPENDENCY));
            deps.appendChild(modDepEl);
        }
        dataEl.appendChild(deps);
        dataEl.appendChild(createModuleElement(dataDoc, PUBLIC_PACKAGES));

        // store document to disk
        ProjectXMLManager.safelyWrite(projectXml, prjDoc);
    }

    /**
     * Create a library wrapper project.xml.
     *
     * @param publicPackages set of <code>String</code>s representing the packages
     * @param extensions &lt;key=runtime path(String), value=binary path (String)&gt;
     */
    static void generateLibraryModuleTemplate(FileObject projectXml, String cnb,
            NbModuleType moduleType, Set<String> publicPackages, Map<String,String> extensions) throws IOException {

        Document prjDoc = XMLUtil.createDocument("project", PROJECT_NS, null, null); // NOI18N

        // generate general project elements
        Element typeEl = prjDoc.createElementNS(PROJECT_NS, "type"); // NOI18N
        typeEl.appendChild(prjDoc.createTextNode(NbModuleProject.TYPE));
        prjDoc.getDocumentElement().appendChild(typeEl);
        Element confEl = prjDoc.createElementNS(PROJECT_NS, "configuration"); // NOI18N
        prjDoc.getDocumentElement().appendChild(confEl);

        // generate NB Module project type specific elements
        Element dataEl = createModuleElement(confEl.getOwnerDocument(), DATA);
        confEl.appendChild(dataEl);
        Document dataDoc = dataEl.getOwnerDocument();
        dataEl.appendChild(createModuleElement(dataDoc, CODE_NAME_BASE, cnb));
        Element moduleTypeEl = createTypeElement(dataDoc, moduleType);
        if (moduleTypeEl != null) {
            dataEl.appendChild(moduleTypeEl);
        }
        dataEl.appendChild(createModuleElement(dataDoc, MODULE_DEPENDENCIES));
        Element packages = createModuleElement(dataDoc, PUBLIC_PACKAGES);
        dataEl.appendChild(packages);
        for (String pkg : publicPackages) {
            packages.appendChild(createModuleElement(dataDoc, PACKAGE, pkg));
        }
        for (Map.Entry<String,String> entry : extensions.entrySet()) {
            Element cp = createModuleElement(dataDoc, CLASS_PATH_EXTENSION);
            dataEl.appendChild(cp);
            cp.appendChild(createModuleElement(dataDoc, CLASS_PATH_RUNTIME_PATH, entry.getKey()));
            cp.appendChild(createModuleElement(dataDoc, CLASS_PATH_BINARY_ORIGIN, entry.getValue()));
        }

        // store document to disk
        ProjectXMLManager.safelyWrite(projectXml, prjDoc);
    }

    private static Element createTypeElement(Document dataDoc, NbModuleType type) {
        Element result = null;
        if (type == NbModuleType.STANDALONE) {
            result = createModuleElement(dataDoc, STANDALONE);
        } else if (type == NbModuleType.SUITE_COMPONENT) {
            result = createModuleElement(dataDoc, SUITE_COMPONENT);
        }
        return result;
    }

    /**
     * Generates a basic <em>project.xml</em> templates into the given
     * <code>projectXml</code> for <em>Suite</em>.
     */
    public static void generateEmptySuiteTemplate(FileObject projectXml, String name) throws IOException {
        // XXX this method could be moved in a future (depends on how complex
        // suite's project.xml will be) to the .suite package dedicated class
        Document prjDoc = XMLUtil.createDocument("project", PROJECT_NS, null, null); // NOI18N

        // generate general project elements
        Element typeEl = prjDoc.createElementNS(PROJECT_NS, "type"); // NOI18N
        typeEl.appendChild(prjDoc.createTextNode(SuiteProjectType.TYPE));
        prjDoc.getDocumentElement().appendChild(typeEl);
        Element confEl = prjDoc.createElementNS(PROJECT_NS, "configuration"); // NOI18N
        prjDoc.getDocumentElement().appendChild(confEl);

        // generate NB Suite project type specific elements
        Element dataEl = createSuiteElement(confEl.getOwnerDocument(), DATA);
        confEl.appendChild(dataEl);
        Document dataDoc = dataEl.getOwnerDocument();
        dataEl.appendChild(createSuiteElement(dataDoc, "name", name)); // NOI18N

        // store document to disk
        ProjectXMLManager.safelyWrite(projectXml, prjDoc);
    }

    private static void safelyWrite(FileObject projectXml, Document prjDoc) throws IOException {
        OutputStream os = projectXml.getOutputStream();
        try {
            XMLUtil.write(prjDoc, os, "UTF-8"); // NOI18N
        } finally {
            os.close();
        }
    }

    private Element getConfData() {
        if (confData == null) {
            confData = project.getPrimaryConfigurationData();
        }
        return confData;
    }
}
