/*
 * $Id$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2026 SARL.io, the original authors and main authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sarl.apputils.uiextensions.classpath;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.jdt.core.IAccessRule;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.util.GenericXMLWriter;
import org.eclipse.jdt.internal.core.ClasspathAccessRule;
import org.eclipse.jdt.internal.core.ClasspathAttribute;
import org.eclipse.jdt.internal.core.ClasspathEntry;
import org.eclipse.jdt.internal.core.ClasspathEntry.AssertionFailedException;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.util.Messages;
import org.eclipse.jdt.internal.core.util.Util;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Class mostly inspired from jdt See {@link JavaProject#decodeClasspath} and {@code ClasspathEntry}.
 *
 * @author $Author: ngaud$
 * @version $FullVersion$
 * @mavengroupid $GroupId$
 * @mavenartifactid $ArtifactId$
 * @since 0.15
 */
public final class JavaClasspathParser {

    /**
     * UnknownXmlElements.
     *
     * @author $Author: ngaud$
     * @version $FullVersion$
     * @mavengroupid $GroupId$
     * @mavenartifactid $ArtifactId$
     */
    static class UnknownXmlElements {
        /**
         * Unknown attributes.
         */
        String[] attributes;

        /**
         * Unknown children.
         */
        List<String> children;
    }

    /**
     * Private constructor.
     */
    private JavaClasspathParser() {
        //
    }

    /**
     * Reads entry of a .classpath file.
     *
     * @param projectName
     *            - the name of project containing the .classpath file
     * @param projectRootAbsoluteFullPath
     *            - the path to project containing the .classpath file
     * @return the set of CLasspath ENtries extracted from the .classpath
     * @throws CoreException
     *             - exception during parsing of .classpath
     * @throws IOException
     *             - exception during parsing of .classpath
     * @throws ClasspathEntry.AssertionFailedException
     *             - exception during parsing of .classpath
     * @throws URISyntaxException
     *             - exception during parsing of .classpath
     */
    public static IClasspathEntry[][] readFileEntriesWithException(String projectName, URL projectRootAbsoluteFullPath)
            throws CoreException, IOException, ClasspathEntry.AssertionFailedException, URISyntaxException {
        return readFileEntriesWithException(projectName, projectRootAbsoluteFullPath, null);
    }

    /**
     * Reads entry of a .classpath file.
     *
     * @param projectName
     *            - the name of project containing the .classpath file
     * @param projectRootAbsoluteFullPath
     *            - the path to project containing the .classpath file
     * @param unknownElements
     *            - map of unknow elements
     * @return the set of CLasspath Entries extracted from the .classpath
     * @throws CoreException
     *             - exception during parsing of .classpath
     * @throws IOException
     *             - exception during parsing of .classpath
     * @throws ClasspathEntry.AssertionFailedException
     *             - exception during parsing of .classpath
     * @throws URISyntaxException
     *             - exception during parsing of .classpath
     */
    public static IClasspathEntry[][] readFileEntriesWithException(String projectName, URL projectRootAbsoluteFullPath,
            Map<IPath, UnknownXmlElements> unknownElements)
            throws CoreException, IOException, ClasspathEntry.AssertionFailedException, URISyntaxException {

        final var rscFile = new URI(
        		projectRootAbsoluteFullPath.toExternalForm().concat(JavaProject.CLASSPATH_FILENAME).replace(" ", "%20")).toURL(); //$NON-NLS-1$ //$NON-NLS-2$
        byte[] bytes;

        // when a project is imported, we get a first delta for the addition of the .project, but the .classpath is not accessible
        // so default to using java.io.File
        // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=96258
        final URI location;
        try {
            location = rscFile.toURI();
        } catch (URISyntaxException e) {
            throw e;
        }
        if (location == null) {
            throw new IOException("Cannot obtain a location URI for " + rscFile); //$NON-NLS-1$
        }
        final File file = Util.toLocalFile(location, null/* no progress monitor available */);
        if (file == null) {
            throw new IOException("Unable to fetch file from " + location); //$NON-NLS-1$
        }

        try {
            bytes = org.eclipse.jdt.internal.compiler.util.Util.getFileByteContent(file);
        } catch (IOException e) {
            throw e;
        }

        if (hasUTF8BOM(bytes)) {
            // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=240034
            final var length = bytes.length - IContentDescription.BOM_UTF_8.length;
            System.arraycopy(bytes, IContentDescription.BOM_UTF_8.length, bytes = new byte[length], 0, length);
        }
        String xmlClasspath;
        try {
            // .classpath always encoded with UTF-8
            xmlClasspath = new String(bytes, org.eclipse.jdt.internal.compiler.util.Util.UTF_8);
        } catch (UnsupportedEncodingException e) {
            Util.log(e, "Could not read .classpath with UTF-8 encoding"); //$NON-NLS-1$
            // fallback to default
            xmlClasspath = new String(bytes);
        }
        return decodeClasspath(projectName, Path.fromPortableString(projectRootAbsoluteFullPath.getPath()), xmlClasspath, unknownElements);
    }

    private static boolean hasUTF8BOM(byte[] bytes) {
        if (bytes.length > IContentDescription.BOM_UTF_8.length) {
            for (int i = 0, length = IContentDescription.BOM_UTF_8.length; i < length; ++i) {
                if (IContentDescription.BOM_UTF_8[i] != bytes[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Reads and decode an XML classpath string. Returns a two-dimensional array, where the number of elements in the row is fixed to 2. The first
     * element is an array of raw classpath entries and the second element is an array of referenced entries that may have been stored by the client
     * earlier. See {@link IJavaProject#getReferencedClasspathEntries()} for more details.
     *
     * @param projectName
     *            - the name of project containing the .classpath file
     * @param projectRootAbsoluteFullPath
     *            - the path to project containing the .classpath file
     * @param xmlClasspath
     *            - path to the XML
     * @param unknownElements
     *            - map of unknow elements
     * @return the set of CLasspath ENtries extracted from the .classpath
     * @throws IOException
     *             - exception during parsing of .classpath
     * @throws ClasspathEntry.AssertionFailedException
     *             - exception during parsing of .classpath
     */
    public static IClasspathEntry[][] decodeClasspath(String projectName, IPath projectRootAbsoluteFullPath, String xmlClasspath,
            Map<IPath, UnknownXmlElements> unknownElements) throws IOException, ClasspathEntry.AssertionFailedException {

        final var paths = new ArrayList<IClasspathEntry>();
        IClasspathEntry defaultOutput = null;
        final Element cpElement;

        try (var reader = new StringReader(xmlClasspath);) {
            final var parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            cpElement = parser.parse(new InputSource(reader)).getDocumentElement();
        } catch (SAXException e) {
            throw new IOException(Messages.file_badFormat);
        } catch (ParserConfigurationException e) {
            throw new IOException(Messages.file_badFormat);
        }

        if (!cpElement.getNodeName().equalsIgnoreCase("classpath")) { //$NON-NLS-1$
            throw new IOException(Messages.file_badFormat);
        }
        var list = cpElement.getElementsByTagName(ClasspathEntry.TAG_CLASSPATHENTRY);
        var length = list.getLength();

        for (var i = 0; i < length; ++i) {
            final var node = list.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                final var entry = elementDecode((Element) node, projectName, projectRootAbsoluteFullPath, unknownElements);
                if (entry != null) {
                    if (entry.getContentKind() == ClasspathEntry.K_OUTPUT) {
                        // separate output
                        defaultOutput = entry;
                    } else {
                        paths.add(entry);
                    }
                }
            }
        }
        final var pathSize = paths.size();
        final var entries = new IClasspathEntry[2][];
        entries[0] = new IClasspathEntry[pathSize + (defaultOutput == null ? 0 : 1)];
        paths.toArray(entries[0]);
        if (defaultOutput != null) {
            // ensure output is last item
            entries[0][pathSize] = defaultOutput;
        }
        paths.clear();
        list = cpElement.getElementsByTagName(ClasspathEntry.TAG_REFERENCED_ENTRY);
        length = list.getLength();

        for (var i = 0; i < length; ++i) {
            final var node = list.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                final var entry = elementDecode((Element) node, projectName, projectRootAbsoluteFullPath, unknownElements);
                if (entry != null) {
                    paths.add(entry);
                }
            }
        }
        entries[1] = new IClasspathEntry[paths.size()];
        paths.toArray(entries[1]);

        return entries;
    }

    /**
     * Decodes one XML element with the XML stream.
     *
     * @param element
     *            - the considered element
     * @param projectName
     *            - the name of project containing the .classpath file
     * @param projectRootAbsoluteFullPath
     *            - he path to project containing the .classpath file
     * @param unknownElements
     *            - map of unknown elements
     * @return the set of CLasspath ENtries extracted from the considered element
     */
    public static IClasspathEntry elementDecode(Element element, String projectName, IPath projectRootAbsoluteFullPath,
            Map<IPath, UnknownXmlElements> unknownElements) {
        final var projectPath = projectRootAbsoluteFullPath;
        final var attributes = element.getAttributes();
        final var children = element.getChildNodes();
        final var foundChildren = new boolean[children.getLength()];
        final var kindAttr = removeAttribute(ClasspathEntry.TAG_KIND, attributes);
        final var pathAttr = removeAttribute(ClasspathEntry.TAG_PATH, attributes);

        // ensure path is absolute
        IPath path = new Path(pathAttr);
        final var kind = kindFromString(kindAttr);
        if (kind != IClasspathEntry.CPE_VARIABLE && kind != IClasspathEntry.CPE_CONTAINER && !path.isAbsolute()) {
            if (!(path.segmentCount() > 0 && path.segment(0).equals(ClasspathEntry.DOT_DOT))) {
                path = projectPath.append(path);
            }
        }
        // source attachment info (optional)
        IPath sourceAttachmentPath = element.hasAttribute(ClasspathEntry.TAG_SOURCEPATH)
                ? new Path(removeAttribute(ClasspathEntry.TAG_SOURCEPATH, attributes)) : null;
        if (kind != IClasspathEntry.CPE_VARIABLE && sourceAttachmentPath != null && !sourceAttachmentPath.isAbsolute()) {
            sourceAttachmentPath = projectPath.append(sourceAttachmentPath);
        }
        final var sourceAttachmentRootPath = element.hasAttribute(ClasspathEntry.TAG_ROOTPATH)
                ? new Path(removeAttribute(ClasspathEntry.TAG_ROOTPATH, attributes)) : null;

        // exported flag (optional)
        final var isExported = removeAttribute(ClasspathEntry.TAG_EXPORTED, attributes).equals("true"); //$NON-NLS-1$

        // inclusion patterns (optional)
        var inclusionPatterns = decodePatterns(attributes, ClasspathEntry.TAG_INCLUDING);
        if (inclusionPatterns == null) {
            inclusionPatterns = ClasspathEntry.INCLUDE_ALL;
        }

        // exclusion patterns (optional)
        var exclusionPatterns = decodePatterns(attributes, ClasspathEntry.TAG_EXCLUDING);
        if (exclusionPatterns == null) {
            exclusionPatterns = ClasspathEntry.EXCLUDE_NONE;
        }

        // access rules (optional)
        var attributeList = getChildAttributes(ClasspathEntry.TAG_ACCESS_RULES, children, foundChildren);
        var accessRules = decodeAccessRules(attributeList);

        // backward compatibility
        if (accessRules == null) {
            accessRules = getAccessRules(inclusionPatterns, exclusionPatterns);
        }

        // combine access rules (optional)
        final var combineAccessRestrictions = !removeAttribute(ClasspathEntry.TAG_COMBINE_ACCESS_RULES, attributes).equals("false"); //$NON-NLS-1$

        // extra attributes (optional)
        attributeList = getChildAttributes(ClasspathEntry.TAG_ATTRIBUTES, children, foundChildren);
        final var extraAttributes = decodeExtraAttributes(attributeList);

        // custom output location
        final var outputLocation = element.hasAttribute(ClasspathEntry.TAG_OUTPUT)
                ? projectPath.append(removeAttribute(ClasspathEntry.TAG_OUTPUT, attributes)) : null;

        String[] unknownAttributes = null;
        ArrayList<String> unknownChildren = null;

        if (unknownElements != null) {
            // unknown attributes
            final var unknownAttributeLength = attributes.getLength();
            if (unknownAttributeLength != 0) {
                unknownAttributes = new String[unknownAttributeLength * 2];
                for (var i = 0; i < unknownAttributeLength; i++) {
                    final var attribute = attributes.item(i);
                    unknownAttributes[i * 2] = attribute.getNodeName();
                    unknownAttributes[i * 2 + 1] = attribute.getNodeValue();
                }
            }

            // unknown children
            for (int i = 0, length = foundChildren.length; i < length; ++i) {
                if (!foundChildren[i]) {
                    final var node = children.item(i);
                    if (node.getNodeType() != Node.ELEMENT_NODE) {
                        continue;
                    }
                    if (unknownChildren == null) {
                        unknownChildren = new ArrayList<>();
                    }
                    final var buffer = new StringBuffer();
                    decodeUnknownNode(node, buffer);
                    unknownChildren.add(buffer.toString());
                }
            }
        }

        // recreate the CP entry
        IClasspathEntry entry = null;
        switch (kind) {

        case IClasspathEntry.CPE_PROJECT:
            /*
             * IPackageFragmentRoot.K_SOURCE, IClasspathEntry.CPE_PROJECT, path, ClasspathEntry.INCLUDE_ALL, // inclusion patterns
             * ClasspathEntry.EXCLUDE_NONE, // exclusion patterns null, // source attachment null, // source attachment root null, // specific output
             * folder
             */
            entry = new ClasspathEntry(IPackageFragmentRoot.K_SOURCE, IClasspathEntry.CPE_PROJECT, path, ClasspathEntry.INCLUDE_ALL,
                    ClasspathEntry.EXCLUDE_NONE, null, null, null, isExported, accessRules, combineAccessRestrictions, extraAttributes);
            break;
        case IClasspathEntry.CPE_LIBRARY:
            entry = JavaCore.newLibraryEntry(path, sourceAttachmentPath, sourceAttachmentRootPath, accessRules, extraAttributes, isExported);
            break;
        case IClasspathEntry.CPE_SOURCE:
            // must be an entry in this project or specify another project
            final var projSegment = path.segment(0);
            if (projSegment != null && projSegment.equals(projectName)) {
                // this project
                entry = JavaCore.newSourceEntry(path, inclusionPatterns, exclusionPatterns, outputLocation, extraAttributes);
            } else {
                if (path.segmentCount() == 1) {
                    // another project
                    entry = JavaCore.newProjectEntry(path, accessRules, combineAccessRestrictions, extraAttributes, isExported);
                } else {
                    // an invalid source folder
                    entry = JavaCore.newSourceEntry(path, inclusionPatterns, exclusionPatterns, outputLocation, extraAttributes);
                }
            }
            break;
        case IClasspathEntry.CPE_VARIABLE:
            entry = JavaCore.newVariableEntry(path, sourceAttachmentPath, sourceAttachmentRootPath, accessRules, extraAttributes, isExported);
            break;
        case IClasspathEntry.CPE_CONTAINER:
            entry = JavaCore.newContainerEntry(path, accessRules, extraAttributes, isExported);
            break;
        case ClasspathEntry.K_OUTPUT:
            if (!path.isAbsolute()) {
                return null;
            }
            /*
             * ClasspathEntry.EXCLUDE_NONE, null, // source attachment null, // source attachment root null, // custom output location false, null, //
             * no access rules false, // no accessible files to combine
             */
            entry = new ClasspathEntry(ClasspathEntry.K_OUTPUT, IClasspathEntry.CPE_LIBRARY, path, ClasspathEntry.INCLUDE_ALL,
                    ClasspathEntry.EXCLUDE_NONE, null, null, null, false, null, false, ClasspathEntry.NO_EXTRA_ATTRIBUTES);
            break;
        default:
            throw new AssertionFailedException(Messages.bind(Messages.classpath_unknownKind, kindAttr));
        }

        if (unknownAttributes != null || unknownChildren != null) {
            final var unknownXmlElements = new UnknownXmlElements();
            unknownXmlElements.attributes = unknownAttributes;
            unknownXmlElements.children = unknownChildren;
            if (unknownElements != null) {
            	unknownElements.put(path, unknownXmlElements);
            }
        }

        return entry;
    }

    private static NodeList getChildAttributes(String childName, NodeList children, boolean[] foundChildren) {
        for (int i = 0, length = foundChildren.length; i < length; ++i) {
            final var node = children.item(i);
            if (childName.equals(node.getNodeName())) {
                foundChildren[i] = true;
                return node.getChildNodes();
            }
        }
        return null;
    }

    private static String removeAttribute(String nodeName, NamedNodeMap nodeMap) {
        final var node = removeNode(nodeName, nodeMap);
        if (node == null) {
            return ""; // //$NON-NLS-1$
        }
        return node.getNodeValue();
    }

    private static Node removeNode(String nodeName, NamedNodeMap nodeMap) {
        try {
            return nodeMap.removeNamedItem(nodeName);
        } catch (DOMException e) {
            if (e.code != DOMException.NOT_FOUND_ERR) {
                throw e;
            }
            return null;
        }
    }

    /**
     * Decode some element tag containing a sequence of patterns into IPath[].
     *
     * @param nodeMap
     *            - map
     * @param tag
     *            - tag
     * @return aarray of IPATH
     */
    private static IPath[] decodePatterns(NamedNodeMap nodeMap, String tag) {
        final var sequence = removeAttribute(tag, nodeMap);
        if (!"".equals(sequence)) { //$NON-NLS-1$
            final var patterns = CharOperation.splitOn('|', sequence.toCharArray());
            final int patternCount;
            if ((patternCount = patterns.length) > 0) {
            	var paths = new IPath[patternCount];
            	var index = 0;
                for (var j = 0; j < patternCount; j++) {
                    final var pattern = patterns[j];
                    if (pattern.length == 0) {
                        // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=105581
                        continue;
                    }
                    paths[index++] = new Path(new String(pattern));
                }
                if (index < patternCount) {
                    System.arraycopy(paths, 0, paths = new IPath[index], 0, index);
                }
                return paths;
            }
        }
        return null;
    }

    private static IAccessRule[] decodeAccessRules(NodeList list) {
        if (list == null) {
            return null;
        }
        final var length = list.getLength();
        if (length == 0) {
            return null;
        }
        var result = new IAccessRule[length];
        var index = 0;
        for (var i = 0; i < length; i++) {
            final var accessRule = list.item(i);
            if (accessRule.getNodeType() == Node.ELEMENT_NODE) {
                final var elementAccessRule = (Element) accessRule;
                final var pattern = elementAccessRule.getAttribute(ClasspathEntry.TAG_PATTERN);
                if (pattern == null) {
                    continue;
                }
                final var tagKind = elementAccessRule.getAttribute(ClasspathEntry.TAG_KIND);
                final int kind;
                if (ClasspathEntry.TAG_ACCESSIBLE.equals(tagKind)) {
                    kind = IAccessRule.K_ACCESSIBLE;
                } else if (ClasspathEntry.TAG_NON_ACCESSIBLE.equals(tagKind)) {
                    kind = IAccessRule.K_NON_ACCESSIBLE;
                } else if (ClasspathEntry.TAG_DISCOURAGED.equals(tagKind)) {
                    kind = IAccessRule.K_DISCOURAGED;
                } else {
                    continue;
                }
                final var ignoreIfBetter = "true".equals(elementAccessRule.getAttribute(ClasspathEntry.TAG_IGNORE_IF_BETTER)); //$NON-NLS-1$
                result[index++] = new ClasspathAccessRule(new Path(pattern), ignoreIfBetter ? kind | IAccessRule.IGNORE_IF_BETTER : kind);
            }
        }
        if (index != length) {
            System.arraycopy(result, 0, result = new IAccessRule[index], 0, index);
        }
        return result;
    }

    private static IClasspathAttribute[] decodeExtraAttributes(NodeList attributes) {
        if (attributes == null) {
            return ClasspathEntry.NO_EXTRA_ATTRIBUTES;
        }
        final var length = attributes.getLength();
        if (length == 0) {
            return ClasspathEntry.NO_EXTRA_ATTRIBUTES;
        }
        var result = new IClasspathAttribute[length];
        var index = 0;
        for (var i = 0; i < length; ++i) {
            final var node = attributes.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                final var attribute = (Element) node;
                final var name = attribute.getAttribute(ClasspathEntry.TAG_ATTRIBUTE_NAME);
                if (name == null) {
                    continue;
                }
                final var value = attribute.getAttribute(ClasspathEntry.TAG_ATTRIBUTE_VALUE);
                if (value == null) {
                    continue;
                }
                result[index++] = new ClasspathAttribute(name, value);
            }
        }
        if (index != length) {
            System.arraycopy(result, 0, result = new IClasspathAttribute[index], 0, index);
        }
        return result;
    }

    private static void decodeUnknownNode(Node node, StringBuffer buffer) {
        final var s = new ByteArrayOutputStream();
        final OutputStreamWriter writer;
        try {
            writer = new OutputStreamWriter(s, "UTF8"); //$NON-NLS-1$
            try (var xmlWriter = new GenericXMLWriter(writer, System.getProperty("line.separator"), //$NON-NLS-1$
                    false/* don't print XML version */)) {
                decodeUnknownNode(node, xmlWriter, true/* insert new line */);
                xmlWriter.flush();
            }
            buffer.append(s.toString("UTF8")); //$NON-NLS-1$
        } catch (UnsupportedEncodingException e) {
            // ignore (UTF8 is always supported)
        }
    }

    private static void decodeUnknownNode(Node node, GenericXMLWriter xmlWriter, boolean insertNewLine) {
        switch (node.getNodeType()) {
        case Node.ELEMENT_NODE:
            final NamedNodeMap attributes;
            HashMap<String, String> parameters = null;
            if ((attributes = node.getAttributes()) != null) {
                final var length = attributes.getLength();
                if (length > 0) {
                    parameters = new HashMap<>();
                    for (var i = 0; i < length; i++) {
                        final var attribute = attributes.item(i);
                        parameters.put(attribute.getNodeName(), attribute.getNodeValue());
                    }
                }
            }
            final var children = node.getChildNodes();
            final var childrenLength = children.getLength();
            final var nodeName = node.getNodeName();
            xmlWriter.printTag(nodeName, parameters, false/* don't insert tab */, false/* don't insert new line */,
                    childrenLength == 0/* close tag if no children */);
            if (childrenLength > 0) {
                for (var i = 0; i < childrenLength; i++) {
                    decodeUnknownNode(children.item(i), xmlWriter, false/* don't insert new line */);
                }
                xmlWriter.endTag(nodeName, false/* don't insert tab */, insertNewLine);
            }
            break;
        case Node.TEXT_NODE:
            final var data = ((Text) node).getData();
            xmlWriter.printString(data, false/* don't insert tab */, false/* don't insert new line */);
            break;
        default:
            break;
        }
    }

    /**
     * Returns the kind of a {@code PackageFragmentRoot} from its {@code String} form.
     *
     * @param kindStr
     *            - string to test
     * @return the integer identifier of the type of the specified string: CPE_PROJECT, CPE_VARIABLE, CPE_CONTAINER, etc.
     */
    private static int kindFromString(String kindStr) {
        if (kindStr.equalsIgnoreCase("prj")) { //$NON-NLS-1$
            return IClasspathEntry.CPE_PROJECT;
        }
        if (kindStr.equalsIgnoreCase("var")) { //$NON-NLS-1$
            return IClasspathEntry.CPE_VARIABLE;
        }
        if (kindStr.equalsIgnoreCase("con")) { //$NON-NLS-1$
            return IClasspathEntry.CPE_CONTAINER;
        }
        if (kindStr.equalsIgnoreCase("src")) { //$NON-NLS-1$
            return IClasspathEntry.CPE_SOURCE;
        }
        if (kindStr.equalsIgnoreCase("lib")) { //$NON-NLS-1$
            return IClasspathEntry.CPE_LIBRARY;
        }
        if (kindStr.equalsIgnoreCase("output")) { //$NON-NLS-1$
            return ClasspathEntry.K_OUTPUT;
        }
        return -1;
    }

    /*
     * Backward compatibility: only accessible and non-accessible files are suported.
     */
    private static IAccessRule[] getAccessRules(IPath[] accessibleFiles, IPath[] nonAccessibleFiles) {
        final var accessibleFilesLength = accessibleFiles == null ? 0 : accessibleFiles.length;
        final var nonAccessibleFilesLength = nonAccessibleFiles == null ? 0 : nonAccessibleFiles.length;
        final var length = accessibleFilesLength + nonAccessibleFilesLength;
        if (length == 0) {
            return null;
        }
        final var accessRules = new IAccessRule[length];
        if (accessibleFiles != null) {
	        for (var i = 0; i < accessibleFilesLength; i++) {
	            accessRules[i] = JavaCore.newAccessRule(accessibleFiles[i], IAccessRule.K_ACCESSIBLE);
	        }
        }
        if (nonAccessibleFiles != null) {
	        for (var i = 0; i < nonAccessibleFilesLength; i++) {
	            accessRules[accessibleFilesLength + i] = JavaCore.newAccessRule(nonAccessibleFiles[i], IAccessRule.K_NON_ACCESSIBLE);
	        }
        }
        return accessRules;
    }

}
