Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
kerby-backend/ldap-backend/src/main/java/org/apache/kerby/kerberos/kdc/identitybackend/LdapIdentityBackend.java
### 代码内容：
/**
 *  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.kerby.kerberos.kdc.identitybackend;

import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.EntryCursor;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.message.ModifyRequest;
import org.apache.directory.api.ldap.model.message.ModifyRequestImpl;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.util.GeneralizedTime;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.shared.kerberos.KerberosAttribute;
import org.apache.kerby.config.Config;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.identity.backend.AbstractIdentityBackend;
import org.apache.kerby.kerberos.kerb.request.KrbIdentity;
import org.apache.kerby.kerberos.kerb.type.KerberosTime;
import org.apache.kerby.kerberos.kerb.type.base.EncryptionKey;
import org.apache.kerby.kerberos.kerb.type.base.EncryptionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

/**
 * An LDAP based backend implementation.
 */
public class LdapIdentityBackend extends AbstractIdentityBackend {
    private int currentConnectionIndex = 0;
    private Map<String, LdapConnection> connections = new HashMap<>();

    //The LdapConnection, may be LdapNetworkConnection or LdapCoreSessionConnection
    private LdapConnection connection;
    private String[] hosts;
    //This is used as a flag to represent the connection whether is
    // LdapNetworkConnection object or not
    private boolean isLdapNetworkConnection;
    private static final Logger LOG = LoggerFactory.getLogger(LdapIdentityBackend.class);

    public LdapIdentityBackend() {
        this.isLdapNetworkConnection = true;
    }

    /**
     * Constructing an instance using specified config that contains anything
     * to be used to initialize an LdapConnection and necessary baseDn.
     * @param config . The config is used to config the backend.
     */
    public LdapIdentityBackend(Config config) {
        setConfig(config);
        this.isLdapNetworkConnection = true;
    }

    /**
     * Constructing an instance using a LdapConnection and a specified config
     * that contains anything to be used to initialize a necessary baseDn.
     * @param config The config is used to config the backend
     * @param connection The connection to be used to handle the operations,
     *                   may be a LdapNetworkConnection or a LdapCoreSessionConnection.
     */
    public LdapIdentityBackend(Config config,
                               LdapConnection connection) {
        setConfig(config);
        if (isHa()) {
            throw new IllegalArgumentException("Only one ldap connection provided, but two needed "
                    + "when set ha hosts[" + config.getString("host") + "]");
        }
        this.connection = connection;
        String hostConfig = getConfig().getString("host");
        hosts = new String[]{hostConfig};
        connections.put(hostConfig, connection);
    }

    public LdapIdentityBackend(Config config, Map<String, LdapConnection> connections) {
        setConfig(config);
        String hostConfig = getConfig().getString("host");
        hosts = hostConfig.trim().split(",");
        if (hosts.length > 2) {
            throw new IllegalArgumentException("More than two ldap hosts is not supported.");
        }
        if (hosts.length != connections.size()) {
            throw new IllegalArgumentException("Number of ldap hosts[" + hosts.length
                    + "] not equal to ldap connections[" + connections.size() + "].");
        }
        this.connections = new HashMap<>(connections);
        connection = connections.get(hosts[currentConnectionIndex]);
    }

    /**
     * Start the connection for the initialize()
     */
    private void startConnection() throws LdapException {
        if (isLdapNetworkConnection) {
            String hostConfig = getConfig().getString("host");
            hosts = hostConfig.trim().split(",");
            if (hosts.length > 2) {
                throw new IllegalArgumentException("More than two ldap hosts is not supported.");
            }
            for (String host: hosts) {
                LdapConnection connection =
                        new LdapNetworkConnection(host, getConfig().getInt("port"));
                connections.put(host, connection);
            }
            String currentHost = hosts[currentConnectionIndex];
            connection = connections.get(currentHost);
        }
        for (LdapConnection connection: connections.values()) {
            connection.bind(getConfig().getString("admin_dn"),
                    getConfig().getString("admin_pw"));
        }
        LOG.info("Start connection with ldap host[" + getConfig().getString("host") + "]");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doInitialize() throws KrbException {
        LOG.info("Initializing the Ldap identity backend.");
        try {
            startConnection();
        } catch (LdapException e) {
            LOG.error("Failed to start connection with LDAP", e);
            throw new KrbException("Failed to start connection with LDAP", e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doStop() throws KrbException {
        try {
            closeConnection();
        } catch (IOException e) {
            LOG.error("Failed to close connection with LDAP", e);
            throw new KrbException("Failed to close connection with LDAP", e);
        }
         LOG.info("closed connection with LDAP.");
    }

    /**
     * Close the connection for stop()
     */
    private void closeConnection() throws IOException {
        IOException cause = null;
        for (Map.Entry<String, LdapConnection> entry: connections.entrySet()) {
            LdapConnection connection = entry.getValue();
            try {
                if (connection.isConnected()) {
                    connection.close();
                }
            } catch (IOException e) {
                LOG.error("Catch exception when close connection with host[" + entry.getKey() + "].");
                if (cause == null) {
                    cause = e;
                } else {
                    cause.addSuppressed(e);
                }
            }
        }
        if (cause != null) {
            throw cause;
        }
    }

    /**
     * Convert a KerberosTime type obeject to a generalized time form of String
     * @param kerberosTime The kerberostime to convert
     */
    private String toGeneralizedTime(KerberosTime kerberosTime) {
        GeneralizedTime generalizedTime = new GeneralizedTime(kerberosTime.getValue());
        return generalizedTime.toString();
    }

    /**
     * An inner class, used to encapsulate key information
     */
    static class KeysInfo {
        private String[] etypes;
        private byte[][] keys;
        private String[] kvnos;

        KeysInfo(KrbIdentity identity) throws KrbException {
            Map<EncryptionType, EncryptionKey> keymap = identity.getKeys();
            this.etypes = new String[keymap.size()];
            this.keys = new byte[keymap.size()][];
            this.kvnos = new String[keymap.size()];
            int i = 0;
            for (Map.Entry<EncryptionType, EncryptionKey> entryKey : keymap.entrySet()) {
                etypes[i] = entryKey.getKey().getValue() + "";
                try {
                    keys[i] = entryKey.getValue().encode();
                } catch (IOException e) {
                    throw new KrbException("encode key failed", e);
                }
                kvnos[i] = entryKey.getValue().getKvno() + "";
                i++;
            }
        }

        public String[] getEtypes() {
            return etypes;
        }

        public byte[][] getKeys() {
            return keys;
        }

        public String[] getKvnos() {
            return kvnos;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected KrbIdentity doAddIdentity(KrbIdentity identity) throws KrbException {
        String principalName = identity.getPrincipalName();
        String[] names = principalName.split("@");
        Entry entry = new DefaultEntry();
        KeysInfo keysInfo = new KeysInfo(identity);
        try {
            Dn dn = toDn(principalName);
            entry.setDn(dn);
            entry.add("objectClass", "top", "person", "inetOrgPerson",
                    "krb5principal", "krb5kdcentry");
            entry.add("uid", names[0]);
            entry.add("cn", names[0]);
            entry.add("sn", names[0]);
            entry.add(KerberosAttribute.KRB5_KEY_AT, keysInfo.getKeys());
            entry.add("krb5EncryptionType", keysInfo.getEtypes());
            entry.add(KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, principalName);
            entry.add(KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT,
                    identity.getKeyVersion() + "");
            entry.add("krb5KDCFlags", "" + identity.getKdcFlags());
            entry.add(KerberosAttribute.KRB5_ACCOUNT_DISABLED_AT, ""
                    + identity.isDisabled());
            entry.add(KerberosAttribute.KRB5_ACCOUNT_LOCKEDOUT_AT, ""
                    + identity.isLocked());
            entry.add(KerberosAttribute.KRB5_ACCOUNT_EXPIRATION_TIME_AT,
                    toGeneralizedTime(identity.getExpireTime()));
            new FailoverInvocationHandler<Void>() {
                @Override
                public Void execute() throws LdapException {
                    connection.add(entry);
                    return null;
                }
            }.run();
        } catch (LdapInvalidDnException e) {
            LOG.error("Error occurred while adding identity", e);
            throw new KrbException("Failed to add identity", e);
        } catch (LdapException e) {
            LOG.error("Error occurred while adding identity", e);
            throw new KrbException("Failed to add identity", e);
        }
        return getIdentity(principalName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected KrbIdentity doGetIdentity(String principalName) throws KrbException {
        KrbIdentity krbIdentity = new KrbIdentity(principalName);
        String searchFilter =
            String.format("(&(objectclass=krb5principal)(krb5PrincipalName=%s))", principalName);
        try {
            EntryCursor cursor = new FailoverInvocationHandler<EntryCursor>() {
                @Override
                public EntryCursor execute() throws LdapException {
                    return connection.search(getConfig().getString("base_dn"), searchFilter,
                        SearchScope.SUBTREE, "dn");
                }
            }.run();

            // there should be at most one entry with this principal name
            if (cursor == null || !cursor.next()) {
                return null;
            }
            Dn dn = cursor.get().getDn();
            cursor.close();

            Entry entry = new FailoverInvocationHandler<Entry>() {
                @Override
                public Entry execute() throws LdapException {
                    return connection.lookup(dn, "*", "+");
                }
            }.run();
            if (entry == null) {
                return null;
            }

            LdapIdentityGetHelper getHelper = new LdapIdentityGetHelper(entry);
            krbIdentity.setPrincipal(getHelper.getPrincipalName());
            krbIdentity.setKeyVersion(getHelper.getKeyVersion());
            krbIdentity.addKeys(getHelper.getKeys());
            krbIdentity.setCreatedTime(getHelper.getCreatedTime());
            krbIdentity.setExpireTime(getHelper.getExpireTime());
            krbIdentity.setDisabled(getHelper.getDisabled());
            krbIdentity.setKdcFlags(getHelper.getKdcFlags());
            krbIdentity.setLocked(getHelper.getLocked());
        } catch (LdapException e) {
            throw new KrbException("Failed to retrieve identity", e);
        } catch (CursorException e) {
            throw new KrbException("Failed to retrieve identity", e);
        } catch (ParseException e) {
            throw new KrbException("Failed to retrieve identity", e);
        } catch (IOException e) {
            throw new KrbException("Failed to retrieve identity", e);
        }

        return krbIdentity;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected KrbIdentity doUpdateIdentity(KrbIdentity identity) throws KrbException {
        String principalName = identity.getPrincipalName();
        KeysInfo keysInfo = new KeysInfo(identity);
        try {
            Dn dn = toDn(principalName);
            ModifyRequest modifyRequest = new ModifyRequestImpl();
            modifyRequest.setName(dn);
            modifyRequest.replace(KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT,
                    "" + identity.getKeyVersion());
            modifyRequest.replace(KerberosAttribute.KRB5_KEY_AT, keysInfo.getKeys());
            modifyRequest.replace("krb5EncryptionType", keysInfo.getEtypes());
            modifyRequest.replace(KerberosAttribute.KRB5_PRINCIPAL_NAME_AT,
                    identity.getPrincipalName());
            modifyRequest.replace(KerberosAttribute.KRB5_ACCOUNT_EXPIRATION_TIME_AT,
                    toGeneralizedTime(identity.getExpireTime()));
            modifyRequest.replace(KerberosAttribute.KRB5_ACCOUNT_DISABLED_AT, ""
                    + identity.isDisabled());
            modifyRequest.replace("krb5KDCFlags", "" + identity.getKdcFlags());
            modifyRequest.replace(KerberosAttribute.KRB5_ACCOUNT_LOCKEDOUT_AT, ""
                    + identity.isLocked());
            new FailoverInvocationHandler<Void>() {
                @Override
                public Void execute() throws LdapException {
                    connection.modify(modifyRequest);
                    return null;
                }
            }.run();
        } catch (LdapException e) {
            LOG.error("Error occurred while updating identity: " + principalName, e);
            throw new KrbException("Failed to update identity", e);
        }

        return getIdentity(principalName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doDeleteIdentity(String principalName) throws KrbException {
        try {
            Dn dn = toDn(principalName);
            new FailoverInvocationHandler<Void>() {
                @Override
                public Void execute() throws LdapException {
                    connection.delete(dn);
                    return null;
                }
            }.run();
        } catch (LdapException e) {
            LOG.error("Error occurred while deleting identity: " + principalName);
            throw new KrbException("Failed to remove identity", e);
        }
    }

    /**
     * Used to convert a dn of String to a Dn object
     * @param principalName The principal name to be convert.
     * @return
     * @throws org.apache.directory.api.ldap.model.exception.LdapInvalidDnException if a remote exception occurs.
     * @throws LdapInvalidAttributeValueException
     */
    private Dn toDn(String principalName) throws LdapInvalidDnException, LdapInvalidAttributeValueException {
        String[] names = principalName.split("@");
        String uid = names[0];
        Dn dn = new Dn(new Rdn("uid", uid), new Dn(getConfig().getString("base_dn")));
        return dn;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected Iterable<String> doGetIdentities() {
        List<String> identityNames = new ArrayList<>();
        EntryCursor cursor;
        Entry entry;
        try {
            cursor = new FailoverInvocationHandler<EntryCursor>() {
                @Override
                public EntryCursor execute() throws LdapException {
                    return connection.search(getConfig().getString("base_dn"), "(objectclass=krb5principal)",
                        SearchScope.SUBTREE, KerberosAttribute.KRB5_PRINCIPAL_NAME_AT);
                }
            }.run();
            if (cursor == null) {
                return null;
            }
            while (cursor.next()) {
                entry = cursor.get();
                Attribute krb5PrincipalNameAt = entry.get(KerberosAttribute.KRB5_PRINCIPAL_NAME_AT);
                if (krb5PrincipalNameAt != null) {
                    identityNames.add(krb5PrincipalNameAt.getString());
                }
            }
            cursor.close();
            Collections.sort(identityNames);
        } catch (LdapException e) {
            LOG.error("With LdapException when LdapConnection searching. " + e);
        } catch (CursorException e) {
            LOG.error("With CursorException when EntryCursor getting. " + e);
        } catch (IOException e) {
            LOG.error("With IOException when closing EntryCursor. " + e);
        }
        return identityNames;
    }

    private void performFailover() {
        currentConnectionIndex = (currentConnectionIndex + 1) % hosts.length;
        String host = hosts[currentConnectionIndex];
        connection = connections.get(host);
    }

    private boolean isHa() {
        String host = getConfig().getString("host");
        if (host != null) {
            return host.trim().split(",").length == 2;
        }
        return false;
    }

    abstract class FailoverInvocationHandler<T> {
        public abstract T execute() throws LdapException;

        public T run() throws LdapException {
            try {
                return execute();
            } catch (LdapException e) {
                if (!isHa()) {
                    throw e;
                }
                String host = hosts[currentConnectionIndex];
                LOG.error("Catch exception with ldap host[" + host + "].", e);
                performFailover();
                host = hosts[currentConnectionIndex];
                LOG.info("Failover to ldap host:" + host + ".");
                return execute();
            }
        }
    }
}

Output:
【漏洞类型】注入漏洞（例如，LDAP注入）
【严重性】High
【漏洞摘要】在`doAddIdentity`、`doGetIdentity`、`doUpdateIdentity`和`doDeleteIdentity`方法中，LDAP搜索过滤器的构建直接使用了用户输入的`principalName`。这可能导致攻击者通过构造恶意的`principalName`来注入恶意的LDAP查询，从而可能访问或修改未经授权的数据。
【关键代码模式】`String.format`用于构建LDAP搜索过滤器，直接使用用户输入的变量。
【修复方法说明】使用`FilterBuilder`来构建LDAP搜索过滤器，以防止用户输入的恶意内容导致注入漏洞。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
build.xml
### 代码内容：
<?xml version="1.0" encoding="UTF-8"?>

<project name="plist" default="compile" basedir=".">
  <property name="source"     value="src/main/java" />
  <property name="test-src"   value="src/test/java" />
  <property name="build"      value="target" />
  <property name="doc"        value="doc" />
  <property name="dist"       value="target"  />
  <property name="jar"        value="dd-plist.jar" />

  <target name="init">
    <mkdir dir="${build}/classes" />
    <!--mkdir dir="${build}/test-classes" /-->
    <mkdir dir="${dist}" />
    <mkdir dir="${doc}" />
  </target>

  <target name="clean">
    <delete dir="${build}" />
    <delete dir="${dist}" />
    <delete dir="${doc}"/>
  </target>

  <target name="compile" depends="init">
    <mkdir dir="${build}/classes/" />
    <javac srcdir="${source}"
           destdir="${build}/classes/"
           target="1.5"
           source="1.5"
           debug="on" debuglevel="lines,vars,source"
           includeantruntime="false"
           fork="true" />
  </target>

  <target name="jar" depends="compile">
    <jar destfile="${dist}/${jar}">
     <fileset dir="${build}/classes"/>
    </jar>
  </target>

  <target name="javadoc" depends="init">
    <delete dir="${doc}/javadoc" />
    <javadoc destdir="${doc}/javadoc">
        <fileset dir="${source}"/>
    </javadoc>
  </target>

  <target name="package" depends="jar,javadoc">
    <zip destfile="${dist}/dd-plist.zip">
      <fileset dir="${dist}" includes="${jar}" />
      <fileset dir="." includes="*.txt" />
      <fileset dir="${doc}"/>
    </zip>
  </target>

</project>

## 内容2
### 文件路径：
src/main/java/com/dd/plist/XMLPropertyListParser.java
### 代码内容：
package com.dd.plist;

import org.w3c.dom.*;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;

/**
 * Parses XML property lists.
 *
 * @author Daniel Dreibrodt
 */
public class XMLPropertyListParser {

    private static DocumentBuilderFactory docBuilderFactory = null;

    /**
     * Instantiation is prohibited by outside classes.
     */
    protected XMLPropertyListParser() {
        /** empty **/
    }

    /**
     * Initialize the document builder factory so that it can be reused and does not need to
     * be reinitialized for each parse action.
     */
    private static synchronized void initDocBuilderFactory() {
        docBuilderFactory = DocumentBuilderFactory.newInstance();
        docBuilderFactory.setIgnoringComments(true);
        docBuilderFactory.setCoalescing(true);
    }

    /**
     * Gets a DocumentBuilder to parse a XML property list.
     * As DocumentBuilders are not thread-safe a new DocBuilder is generated for each request.
     *
     * @return A new DocBuilder that can parse property lists w/o an internet connection.
     * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list
     *                                                        could not be created. This should not occur.
     */
    private static synchronized DocumentBuilder getDocBuilder() throws ParserConfigurationException {
        if (docBuilderFactory == null)
            initDocBuilderFactory();
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        docBuilder.setEntityResolver(new EntityResolver() {
            public InputSource resolveEntity(String publicId, String systemId) {
                if ("-//Apple Computer//DTD PLIST 1.0//EN".equals(publicId) || // older publicId
                        "-//Apple//DTD PLIST 1.0//EN".equals(publicId)) { // newer publicId
                    // return a dummy, zero length DTD so we don't have to fetch
                    // it from the network.
                    return new InputSource(new ByteArrayInputStream(new byte[0]));
                }
                return null;
            }
        });
        return docBuilder;
    }

    /**
     * Parses a XML property list file.
     *
     * @param f The XML property list file.
     * @return The root object of the property list. This is usually a NSDictionary but can also be a NSArray.
     * @see javax.xml.parsers.DocumentBuilder#parse(java.io.File)
     * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list
     *                                                        could not be created. This should not occur.
     * @throws java.io.IOException If any IO error occurs while reading the file.
     * @throws org.xml.sax.SAXException If any parse error occurs.
     * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format.
     * @throws java.text.ParseException If a date string could not be parsed.
     */
    public static NSObject parse(File f) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
        DocumentBuilder docBuilder = getDocBuilder();

        Document doc = docBuilder.parse(f);

        return parse(doc);
    }

    /**
     * Parses a XML property list from a byte array.
     *
     * @param bytes The byte array containing the property list's data.
     * @return The root object of the property list. This is usually a NSDictionary but can also be a NSArray.
     * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list
     *                                                        could not be created. This should not occur.
     * @throws java.io.IOException If any IO error occurs while reading the file.
     * @throws org.xml.sax.SAXException If any parse error occurs.
     * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format.
     * @throws java.text.ParseException If a date string could not be parsed.
     */
    public static NSObject parse(final byte[] bytes) throws ParserConfigurationException, ParseException, SAXException, PropertyListFormatException, IOException {
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        return parse(bis);
    }

    /**
     * Parses a XML property list from an input stream.
     *
     * @param is The input stream pointing to the property list's data.
     * @return The root object of the property list. This is usually a NSDictionary but can also be a NSArray.
     * @see javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream)
     * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list
     *                                                        could not be created. This should not occur.
     * @throws java.io.IOException If any IO error occurs while reading the file.
     * @throws org.xml.sax.SAXException If any parse error occurs.
     * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format.
     * @throws java.text.ParseException If a date string could not be parsed.
     */
    public static NSObject parse(InputStream is) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
        DocumentBuilder docBuilder = getDocBuilder();

        Document doc = docBuilder.parse(is);

        return parse(doc);
    }

    /**
     * Parses a property list from an XML document.
     *
     * @param doc The XML document.
     * @return The root NSObject of the property list contained in the XML document.
     * @throws java.io.IOException If any IO error occurs while reading the file.
     * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format.
     * @throws java.text.ParseException If a date string could not be parsed.
     */
    public static NSObject parse(Document doc) throws PropertyListFormatException, IOException, ParseException {
        DocumentType docType = doc.getDoctype();
        if (docType == null) {
            if (!doc.getDocumentElement().getNodeName().equals("plist")) {
                throw new UnsupportedOperationException("The given XML document is not a property list.");
            }
        } else if (!docType.getName().equals("plist")) {
            throw new UnsupportedOperationException("The given XML document is not a property list.");
        }

        Node rootNode;

        if (doc.getDocumentElement().getNodeName().equals("plist")) {
            //Root element wrapped in plist tag
            List<Node> rootNodes = filterElementNodes(doc.getDocumentElement().getChildNodes());
            if (rootNodes.isEmpty()) {
                throw new PropertyListFormatException("The given XML property list has no root element!");
            } else if (rootNodes.size() == 1) {
                rootNode = rootNodes.get(0);
            } else {
                throw new PropertyListFormatException("The given XML property list has more than one root element!");
            }
        } else {
            //Root NSObject not wrapped in plist-tag
            rootNode = doc.getDocumentElement();
        }

        return parseObject(rootNode);
    }

    /**
     * Parses a node in the XML structure and returns the corresponding NSObject
     *
     * @param n The XML node.
     * @return The corresponding NSObject.
     * @throws java.io.IOException If any IO error occurs while parsing a Base64 encoded NSData object.
     * @throws java.text.ParseException If a date string could not be parsed.
     */
    private static NSObject parseObject(Node n) throws ParseException, IOException {
        String type = n.getNodeName();
        if (type.equals("dict")) {
            NSDictionary dict = new NSDictionary();
            List<Node> children = filterElementNodes(n.getChildNodes());
            for (int i = 0; i < children.size(); i += 2) {
                Node key = children.get(i);
                Node val = children.get(i + 1);

                String keyString = getNodeTextContents(key);

                dict.put(keyString, parseObject(val));
            }
            return dict;
        } else if (type.equals("array")) {
            List<Node> children = filterElementNodes(n.getChildNodes());
            NSArray array = new NSArray(children.size());
            for (int i = 0; i < children.size(); i++) {
                array.setValue(i, parseObject(children.get(i)));
            }
            return array;
        } else if (type.equals("true")) {
            return new NSNumber(true);
        } else if (type.equals("false")) {
            return new NSNumber(false);
        } else if (type.equals("integer")) {
            return new NSNumber(getNodeTextContents(n));
        } else if (type.equals("real")) {
            return new NSNumber(getNodeTextContents(n));
        } else if (type.equals("string")) {
            return new NSString(getNodeTextContents(n));
        } else if (type.equals("data")) {
            return new NSData(getNodeTextContents(n));
        } else if (type.equals("date")) {
            return new NSDate(getNodeTextContents(n));
        }
        return null;
    }

    /**
     * Returns all element nodes that are contained in a list of nodes.
     *
     * @param list The list of nodes to search.
     * @return The sub-list containing only nodes representing actual elements.
     */
    private static List<Node> filterElementNodes(NodeList list) {
        List<Node> result = new ArrayList<Node>(list.getLength());
        for (int i = 0; i < list.getLength(); i++) {
            if (list.item(i).getNodeType() == Node.ELEMENT_NODE) {
                result.add(list.item(i));
            }
        }
        return result;
    }

    /**
     * Returns a node's text content.
     * This method will return the text value represented by the node's direct children.
     * If the given node is a TEXT or CDATA node, then its value is returned.
     *
     * @param n The node.
     * @return The node's text content.
     */
    private static String getNodeTextContents(Node n) {
        if (n.getNodeType() == Node.TEXT_NODE || n.getNodeType() == Node.CDATA_SECTION_NODE) {
            Text txtNode = (Text) n;
            String content = txtNode.getWholeText(); //This concatenates any adjacent text/cdata/entity nodes
            if (content == null)
                return "";
            else
                return content;
        } else {
            if (n.hasChildNodes()) {
                NodeList children = n.getChildNodes();

                for (int i = 0; i < children.getLength(); i++) {
                    //Skip any non-text nodes, like comments or entities
                    Node child = children.item(i);
                    if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) {
                        Text txtNode = (Text) child;
                        String content = txtNode.getWholeText(); //This concatenates any adjacent text/cdata/entity nodes
                        if (content == null)
                            return "";
                        else
                            return content;
                    }
                }

                return "";
            } else {
                return "";
            }
        }
    }
}