public final class XsdGeneratorHelper {

    // Constants
    private static final String MISCONFIG = "Misconfiguration detected: ";
    private static TransformerFactory FACTORY;
    private static final FileFilter RECURSIVE_XSD_FILTER;

    /**
     * Hide the constructor for utility classes.
     */
    private XsdGeneratorHelper() {
        // Do nothing.
    }

    static {

        // Create the static filter used for recursive generated XSD files detection.
        RECURSIVE_XSD_FILTER = new FileFilter() {
            @Override
            public boolean accept(final File toMatch) {

                if (toMatch.exists()) {

                    // Accept directories for recursive operation, and
                    // files with names matching the SCHEMAGEN_EMITTED_FILENAME Pattern.
                    return toMatch.isDirectory()
                            || AbstractXsdGeneratorMojo.SCHEMAGEN_EMITTED_FILENAME.matcher(toMatch.getName()).matches();
                }

                // Not a directory or XSD file.
                return false;
            }
        };
    }

    /**
     * Acquires a map relating generated schema filename to its SimpleNamespaceResolver.
     *
     * @param outputDirectory The output directory of the generated schema files.
     * @return a map relating generated schema filename to an initialized SimpleNamespaceResolver.
     * @throws MojoExecutionException if two generated schema files used the same namespace URI.
     */
    public static Map<String, SimpleNamespaceResolver> getFileNameToResolverMap(final File outputDirectory)
            throws MojoExecutionException {

        final Map<String, SimpleNamespaceResolver> toReturn = new TreeMap<String, SimpleNamespaceResolver>();

        // Each generated schema file should be written to the output directory.
        // Each generated schema file should have a unique targetNamespace.
        File[] generatedSchemaFiles = outputDirectory.listFiles(new FileFilter() {
            public boolean accept(File pathname) {
                return pathname.getName().startsWith("schema") && pathname.getName().endsWith(".xsd");
            }
        });

        for (File current : generatedSchemaFiles) {
            toReturn.put(current.getName(), new SimpleNamespaceResolver(current));
        }

        return toReturn;
    }

    /**
     * Validates that the list of Schemas provided within the configuration all contain unique values. Should a
     * MojoExecutionException be thrown, it contains informative text about the exact nature of the configuration
     * problem - we should simplify for all plugin users.
     *
     * @param configuredTransformSchemas The List of configuration schemas provided to this mojo.
     * @throws MojoExecutionException if any two configuredSchemas instances contain duplicate values for any of the
     *                                properties uri, prefix or file. Also throws a MojoExecutionException if the uri of any Schema is null
     *                                or empty, or if none of the 'file' and 'prefix' properties are given within any of the
     *                                configuredSchema instances.
     */
    public static void validateSchemasInPluginConfiguration(final List<TransformSchema> configuredTransformSchemas)
            throws MojoExecutionException {

        final List<String> uris = new ArrayList<String>();
        final List<String> prefixes = new ArrayList<String>();
        final List<String> fileNames = new ArrayList<String>();

        for (int i = 0; i < configuredTransformSchemas.size(); i++) {
            final TransformSchema current = configuredTransformSchemas.get(i);
            final String currentURI = current.getUri();
            final String currentPrefix = current.getToPrefix();
            final String currentFile = current.getToFile();

            // We cannot work with a null or empty uri
            if (StringUtils.isEmpty(currentURI)) {
                throw new MojoExecutionException(MISCONFIG + "Null or empty property 'uri' found in "
                        + "plugin configuration for schema element at index [" + i + "]: " + current);
            }

            // No point in having *only* a namespace.
            if (StringUtils.isEmpty(currentPrefix) && StringUtils.isEmpty(currentFile)) {
                throw new MojoExecutionException(MISCONFIG + "Null or empty properties 'prefix' "
                        + "and 'file' found within plugin configuration for schema element at index ["
                        + i + "]: " + current);
            }

            // Validate that all given uris are unique.
            if (uris.contains(currentURI)) {
                throw new MojoExecutionException(getDuplicationErrorMessage("uri", currentURI,
                        uris.indexOf(currentURI), i));
            }
            uris.add(currentURI);

            // Validate that all given prefixes are unique.
            if (prefixes.contains(currentPrefix) && !(currentPrefix == null)) {
                throw new MojoExecutionException(getDuplicationErrorMessage("prefix", currentPrefix,
                        prefixes.indexOf(currentPrefix), i));
            }
            prefixes.add(currentPrefix);

            // Validate that all given files are unique.
            if (fileNames.contains(currentFile)) {
                throw new MojoExecutionException(getDuplicationErrorMessage("file", currentFile,
                        fileNames.indexOf(currentFile), i));
            }
            fileNames.add(currentFile);
        }
    }

    /**
     * Inserts XML documentation annotations into all generated XSD files found
     * within the supplied outputDir.
     *
     * @param log       A Maven Log.
     * @param outputDir The outputDir, where generated XSD files are found.
     * @param docs      The SearchableDocumentation for the source files within the compilation unit.
     * @param renderer  The JavaDocRenderer used to convert JavaDoc annotations into XML documentation annotations.
     * @return The number of processed XSDs.
     */
    public static int insertJavaDocAsAnnotations(final Log log,
                                                 final String encoding,
                                                 final File outputDir,
                                                 final SearchableDocumentation docs,
                                                 final JavaDocRenderer renderer) {

        // Check sanity
        Validate.notNull(docs, "docs");
        Validate.notNull(log, "log");
        Validate.notNull(outputDir, "outputDir");
        Validate.isTrue(outputDir.isDirectory(), "'outputDir' must be a Directory.");
        Validate.notNull(renderer, "renderer");

        int processedXSDs = 0;
        final List<File> foundFiles = new ArrayList<File>();
        addRecursively(foundFiles, RECURSIVE_XSD_FILTER, outputDir);

        if (foundFiles.size() > 0) {

            // Create the processors.
            final XsdAnnotationProcessor classProcessor = new XsdAnnotationProcessor(docs, renderer);
            final XsdEnumerationAnnotationProcessor enumProcessor
                    = new XsdEnumerationAnnotationProcessor(docs, renderer);

            for (File current : foundFiles) {

                // Create an XSD document from the current File.
                final Document generatedSchemaFileDocument = parseXmlToDocument(current);

                // Replace all namespace prefixes within the provided document.
                process(generatedSchemaFileDocument.getFirstChild(), true, classProcessor);
                processedXSDs++;

                // Overwrite the vanilla file.
                savePrettyPrintedDocument(generatedSchemaFileDocument, current, encoding);
            }

        } else {
            if (log.isWarnEnabled()) {
                log.warn("Found no generated 'vanilla' XSD files to process under ["
                        + FileSystemUtilities.getCanonicalPath(outputDir) + "]. Aborting processing.");
            }
        }

        // All done.
        return processedXSDs;
    }

    /**
     * Replaces all namespaces within generated schema files, as instructed by the configured Schema instances.
     *
     * @param resolverMap                The map relating generated schema file name to SimpleNamespaceResolver instances.
     * @param configuredTransformSchemas The Schema instances read from the configuration of this plugin.
     * @param mavenLog                   The active Log.
     * @param schemaDirectory            The directory where all generated schema files reside.
     * @param encoding                   The encoding to use when writing the file.
     * @throws MojoExecutionException If the namespace replacement could not be done.
     */
    public static void replaceNamespacePrefixes(
            final Map<String, SimpleNamespaceResolver> resolverMap,
            final List<TransformSchema> configuredTransformSchemas,
            final Log mavenLog,
            final File schemaDirectory,
            final String encoding) throws MojoExecutionException {

        if (mavenLog.isDebugEnabled()) {
            mavenLog.debug("Got resolverMap.keySet() [generated filenames]: " + resolverMap.keySet());
        }

        for (SimpleNamespaceResolver currentResolver : resolverMap.values()) {
            File generatedSchemaFile = new File(schemaDirectory, currentResolver.getSourceFilename());
            Document generatedSchemaFileDocument = null;

            for (TransformSchema currentTransformSchema : configuredTransformSchemas) {
                // Should we alter the namespace prefix as instructed by the current schema?
                final String newPrefix = currentTransformSchema.getToPrefix();
                final String currentUri = currentTransformSchema.getUri();

                if (StringUtils.isNotEmpty(newPrefix)) {
                    // Find the old/current prefix of the namespace for the current schema uri.
                    final String oldPrefix = currentResolver.getNamespaceURI2PrefixMap().get(currentUri);

                    if (StringUtils.isNotEmpty(oldPrefix)) {
                        // Can we perform the prefix substitution?
                        validatePrefixSubstitutionIsPossible(oldPrefix, newPrefix, currentResolver);

                        if (mavenLog.isDebugEnabled()) {
                            mavenLog.debug("Subtituting namespace prefix [" + oldPrefix + "] with [" + newPrefix
                                    + "] in file [" + currentResolver.getSourceFilename() + "].");
                        }

                        // Get the Document of the current schema file.
                        if (generatedSchemaFileDocument == null) {
                            generatedSchemaFileDocument = parseXmlToDocument(generatedSchemaFile);
                        }

                        // Replace all namespace prefixes within the provided document.
                        process(generatedSchemaFileDocument.getFirstChild(), true,
                                new ChangeNamespacePrefixProcessor(oldPrefix, newPrefix));
                    }
                }
            }

            if (generatedSchemaFileDocument != null) {
                // Overwrite the generatedSchemaFile with the content of the generatedSchemaFileDocument.
                mavenLog.debug("Overwriting file [" + currentResolver.getSourceFilename() + "] with content ["
                        + getHumanReadableXml(generatedSchemaFileDocument) + "]");
                savePrettyPrintedDocument(generatedSchemaFileDocument, generatedSchemaFile, encoding);
            } else {
                mavenLog.debug("No namespace prefix changes to generated schema file ["
                        + generatedSchemaFile.getName() + "]");
            }
        }
    }

    /**
     * Updates all schemaLocation attributes within the generated schema files to match the 'file' properties within the
     * Schemas read from the plugin configuration. After that, the files are physically renamed.
     *
     * @param resolverMap                The map relating generated schema file name to SimpleNamespaceResolver instances.
     * @param configuredTransformSchemas The Schema instances read from the configuration of this plugin.
     * @param mavenLog                   The active Log.
     * @param schemaDirectory            The directory where all generated schema files reside.
     * @param charsetName                The encoding / charset name.
     */
    public static void renameGeneratedSchemaFiles(final Map<String, SimpleNamespaceResolver> resolverMap,
                                                  final List<TransformSchema> configuredTransformSchemas,
                                                  final Log mavenLog,
                                                  final File schemaDirectory,
                                                  final String charsetName) {

        // Create the map relating namespace URI to desired filenames.
        Map<String, String> namespaceUriToDesiredFilenameMap = new TreeMap<String, String>();
        for (TransformSchema current : configuredTransformSchemas) {
            if (StringUtils.isNotEmpty(current.getToFile())) {
                namespaceUriToDesiredFilenameMap.put(current.getUri(), current.getToFile());
            }
        }

        // Replace the schemaLocation values to correspond to the new filenames
        for (SimpleNamespaceResolver currentResolver : resolverMap.values()) {
            File generatedSchemaFile = new File(schemaDirectory, currentResolver.getSourceFilename());
            Document generatedSchemaFileDocument = parseXmlToDocument(generatedSchemaFile);

            // Replace all namespace prefixes within the provided document.
            process(generatedSchemaFileDocument.getFirstChild(), true,
                    new ChangeFilenameProcessor(namespaceUriToDesiredFilenameMap));

            // Overwrite the generatedSchemaFile with the content of the generatedSchemaFileDocument.
            if (mavenLog.isDebugEnabled()) {
                mavenLog.debug("Changed schemaLocation entries within [" + currentResolver.getSourceFilename() + "]. "
                        + "Result: [" + getHumanReadableXml(generatedSchemaFileDocument) + "]");
            }
            savePrettyPrintedDocument(generatedSchemaFileDocument, generatedSchemaFile, charsetName);
        }

        // Now, rename the actual files.
        for (SimpleNamespaceResolver currentResolver : resolverMap.values()) {
            final String localNamespaceURI = currentResolver.getLocalNamespaceURI();

            if (StringUtils.isEmpty(localNamespaceURI)) {
                mavenLog.warn("SimpleNamespaceResolver contained no localNamespaceURI; aborting rename.");
                continue;
            }

            final String newFilename = namespaceUriToDesiredFilenameMap.get(localNamespaceURI);
            final File originalFile = new File(schemaDirectory, currentResolver.getSourceFilename());

            if (StringUtils.isNotEmpty(newFilename)) {
                File renamedFile = FileUtils.resolveFile(schemaDirectory, newFilename);
                String renameResult = (originalFile.renameTo(renamedFile) ? "Success " : "Failure ");

                if (mavenLog.isDebugEnabled()) {
                    String suffix = "renaming [" + originalFile.getAbsolutePath() + "] to [" + renamedFile + "]";
                    mavenLog.debug(renameResult + suffix);
                }
            }
        }
    }

    /**
     * Drives the supplied visitor to process the provided Node and all its children, should the recurseToChildren flag
     * be set to <code>true</code>. All attributes of the current node are processed before recursing to children (i.e.
     * breadth first recursion).
     *
     * @param node              The Node to process.
     * @param recurseToChildren if <code>true</code>, processes all children of the supplied node recursively.
     * @param visitor           The NodeProcessor instance which should process the nodes.
     */
    public static void process(final Node node, final boolean recurseToChildren, final NodeProcessor visitor) {

        // Process the current Node, if the NodeProcessor accepts it.
        if (visitor.accept(node)) {
            visitor.process(node);
        }

        NamedNodeMap attributes = node.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
            Node attribute = attributes.item(i);

            // Process the current attribute, if the NodeProcessor accepts it.
            if (visitor.accept(attribute)) {
                visitor.process(attribute);
            }
        }

        if (recurseToChildren) {
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node child = children.item(i);

                // Recurse to Element children.
                if (child.getNodeType() == Node.ELEMENT_NODE) {
                    process(child, true, visitor);
                }
            }
        }
    }

    /**
     * Parses the provided InputStream to create a dom Document.
     *
     * @param xmlStream An InputStream connected to an XML document.
     * @return A DOM Document created from the contents of the provided stream.
     */
    public static Document parseXmlStream(final Reader xmlStream) {

        // Build a DOM model of the provided xmlFileStream.
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);

        try {
            return factory.newDocumentBuilder().parse(new InputSource(xmlStream));
        } catch (Exception e) {
            throw new IllegalArgumentException("Could not acquire DOM Document", e);
        }
    }

    /**
     * Converts the provided DOM Node to a pretty-printed XML-formatted string.
     *
     * @param node The Node whose children should be converted to a String.
     * @return a pretty-printed XML-formatted string.
     */
    protected static String getHumanReadableXml(final Node node) {
        StringWriter toReturn = new StringWriter();

        try {
            Transformer transformer = getFactory().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
            transformer.transform(new DOMSource(node), new StreamResult(toReturn));
        } catch (TransformerException e) {
            throw new IllegalStateException("Could not transform node [" + node.getNodeName() + "] to XML", e);
        }

        return toReturn.toString();
    }

    //
    // Private helpers
    //

    private static String getDuplicationErrorMessage(final String propertyName, final String propertyValue,
                                                     final int firstIndex, final int currentIndex) {
        return MISCONFIG + "Duplicate '" + propertyName + "' property with value [" + propertyValue
                + "] found in plugin configuration. Correct schema elements index (" + firstIndex + ") and ("
                + currentIndex + "), to ensure that all '" + propertyName + "' values are unique.";
    }

    /**
     * Validates that the transformation from <code>oldPrefix</code> to <code>newPrefix</code> is possible, in that
     * <code>newPrefix</code> is not already used by a schema file. This would corrupt the schema by assigning elements
     * from one namespace to another.
     *
     * @param oldPrefix       The old/current namespace prefix.
     * @param newPrefix       The new/future namespace prefix.
     * @param currentResolver The currently active SimpleNamespaceResolver.
     * @throws MojoExecutionException if any schema file currently uses <code>newPrefix</code>.
     */
    private static void validatePrefixSubstitutionIsPossible(final String oldPrefix, final String newPrefix,
                                                             final SimpleNamespaceResolver currentResolver)
            throws MojoExecutionException {
        // Make certain the newPrefix does not exist already.
        if (currentResolver.getNamespaceURI2PrefixMap().containsValue(newPrefix)) {
            throw new MojoExecutionException(MISCONFIG + "Namespace prefix [" + newPrefix + "] is already in use."
                    + " Cannot replace namespace prefix [" + oldPrefix + "] with [" + newPrefix + "] in file ["
                    + currentResolver.getSourceFilename() + "].");
        }
    }

    /**
     * Creates a Document from parsing the XML within the provided xmlFile.
     *
     * @param xmlFile The XML file to be parsed.
     * @return The Document corresponding to the xmlFile.
     */
    private static Document parseXmlToDocument(final File xmlFile) {
        Document result = null;
        Reader reader = null;
        try {
            reader = new FileReader(xmlFile);
            result = parseXmlStream(reader);
        } catch (FileNotFoundException e) {
            // This should never happen...
        } finally {
            IOUtil.close(reader);
        }

        return result;
    }

    private static void savePrettyPrintedDocument(final Document toSave,
                                                  final File targetFile,
                                                  final String charsetName) {
        Writer out = null;
        try {
            out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(targetFile), charsetName));
            out.write(getHumanReadableXml(toSave.getFirstChild()));
        } catch (IOException e) {
            throw new IllegalStateException("Could not write to file [" + targetFile.getAbsolutePath() + "]", e);
        } finally {
            IOUtil.close(out);
        }
    }

    private static void addRecursively(final List<File> toPopulate,
                                       final FileFilter fileFilter,
                                       final File aDir) {

        // Check sanity
        Validate.notNull(toPopulate, "toPopulate");
        Validate.notNull(fileFilter, "fileFilter");
        Validate.notNull(aDir, "aDir");

        // Add all matching files.
        for (File current : aDir.listFiles(fileFilter)) {

            if (current.isFile()) {
                toPopulate.add(current);
            } else if (current.isDirectory()) {
                addRecursively(toPopulate, fileFilter, current);
            }
        }
    }

    private static TransformerFactory getFactory() {

        if (FACTORY == null) {

            try {
                FACTORY = TransformerFactory.newInstance();

                // Harmonize XML formatting
                for (String currentAttributeName : Arrays.asList("indent-number", OutputKeys.INDENT)) {
                    try {
                        FACTORY.setAttribute(currentAttributeName, 2);
                    } catch (IllegalArgumentException ex) {
                        // Ignore this.
                    }
                }
            } catch (Throwable exception) {

                // This should really not happen... but it seems to happen in some test cases.
                throw new IllegalStateException("Could not acquire TransformerFactory implementation.", exception);
            }
        }

        // All done.
        return FACTORY;
    }
} public abstract class AbstractSourceCodeAwareNodeProcessingTest {

    /**
     * Default systemId for the empty namespace.
     */
    public static final String DEFAULT_EMPTY_NAMESPACE_SYSTEM_ID = "emptyNamespaceSystemId.xsd";

    // Shared state
    protected BufferingLog log;
    protected SearchableDocumentation docs;
    protected SortedMap<String, String> namespace2GeneratedSchemaMap;
    protected SortedMap<String, Document> namespace2DocumentMap;
    protected Map<String, String> namespace2SystemIdMap;
    protected List<String> xsdGenerationWarnings;
    protected final File basedir;
    protected final File testJavaDir;
    protected JAXBContext jaxbContext;
    protected SortedMap<String, Throwable> xsdGenerationLog;

    // Internal state
    private List<Class<?>> jaxbClasses;

    public AbstractSourceCodeAwareNodeProcessingTest() {

        // Setup the basic directories.
        basedir = getBasedir();
        testJavaDir = new File(basedir, "src/test/java");
        Assert.assertTrue(testJavaDir.exists() && testJavaDir.isDirectory());
    }

    @Before
    public final void setupSharedState() throws Exception {

        log = new BufferingLog(BufferingLog.LogLevel.DEBUG);

        // Create internal state for the generated structures.
        namespace2SystemIdMap = new TreeMap<String, String>();
        xsdGenerationWarnings = new ArrayList<String>();
        namespace2DocumentMap = new TreeMap<String, Document>();
        namespace2GeneratedSchemaMap = new TreeMap<String, String>();

        // Pre-populate the namespace2SystemIdMap
        namespace2SystemIdMap.put(SomewhatNamedPerson.NAMESPACE, "somewhatNamedPerson.xsd");
        namespace2SystemIdMap.put("http://jaxb.mojohaus.org/wrappers", "wrapperExample.xsd");
        namespace2SystemIdMap.put("http://gnat.west.se/foods", "anotherExample.xsd");
        namespace2SystemIdMap.put("", DEFAULT_EMPTY_NAMESPACE_SYSTEM_ID);

        // Create the JAXBContext
        jaxbClasses = getJaxbAnnotatedClassesForJaxbContext();
        Assert.assertNotNull("getJaxbAnnotatedClassesForJaxbContext() should not return a null List.", jaxbClasses);
        final Class<?>[] classArray = jaxbClasses.toArray(new Class<?>[jaxbClasses.size()]);
        jaxbContext = JAXBContext.newInstance(classArray);

        // Generate the vanilla XSD from JAXB
        final SortedMap<String, StringWriter> tmpSchemaMap = new TreeMap<String, StringWriter>();

        try {
            jaxbContext.generateSchema(new SchemaOutputResolver() {
                @Override
                public Result createOutput(final String namespaceUri,
                                           final String suggestedFileName)
                        throws IOException {

                    // As put in the XmlBinding JAXB implementation of Nazgul Core:
                    //
                    // "The types should really be annotated with @XmlType(namespace = "... something ...")
                    // to avoid using the default ("") namespace".
                    if (namespaceUri.isEmpty()) {
                        xsdGenerationWarnings.add("Got empty namespaceUri for suggestedFileName ["
                                + suggestedFileName + "].");
                    }

                    // Create the result Writer
                    final StringWriter out = new StringWriter();
                    final StreamResult toReturn = new StreamResult(out);

                    // The systemId *must* be non-null, even in this case where we
                    // do not write the XSD to a file.
                    final String effectiveSystemId = namespace2SystemIdMap.get(namespaceUri) == null
                            ? suggestedFileName
                            : namespace2SystemIdMap.get(namespaceUri);
                    toReturn.setSystemId(effectiveSystemId);

                    // Map the namespaceUri to the schemaResult.
                    tmpSchemaMap.put(namespaceUri, out);

                    // All done.
                    return toReturn;
                }
            });
        } catch (IOException e) {
            throw new IllegalArgumentException("Could not acquire Schema snippets.", e);
        }

        // Store all generated XSDs
        for (Map.Entry<String, StringWriter> current : tmpSchemaMap.entrySet()) {
            namespace2GeneratedSchemaMap.put(current.getKey(), current.getValue().toString());
        }

        // Create XML Documents for all generated Schemas
        for (Map.Entry<String, String> current : namespace2GeneratedSchemaMap.entrySet()) {
            final Document document = createDocument(current.getValue());
            namespace2DocumentMap.put(current.getKey(), document);
        }

        // Create the SearchableDocumentation
        final JavaDocExtractor extractor = new JavaDocExtractor(log);
        extractor.addSourceFiles(resolveSourceFiles());
        docs = extractor.process();

        // Stash and clear the log buffer.
        xsdGenerationLog = log.getAndResetLogBuffer();
    }

    /**
     * @return A List containing all classes which should be part of the JAXBContext.
     */
    protected abstract List<Class<?>> getJaxbAnnotatedClassesForJaxbContext();

    /**
     * @return The basedir directory, corresponding to the root of this project.
     */
    protected File getBasedir() {

        // Use the system property if available.
        String basedirPath = System.getProperty("basedir");
        if (basedirPath == null) {
            basedirPath = new File("").getAbsolutePath();
        }

        final File toReturn = new File(basedirPath);
        Assert.assertNotNull("Could not find 'basedir'. Please set the system property 'basedir'.", toReturn);
        Assert.assertTrue("'basedir' must be an existing directory. ", toReturn.exists() && toReturn.isDirectory());

        // All done.
        return toReturn;
    }

    /**
     * Creates a DOM Document from the supplied XML.
     *
     * @param xmlContent The non-empty XML which should be converted into a Document.
     * @return The Document created from the supplied XML Content.
     */
    protected final Document createDocument(final String xmlContent) {

        // Check sanity
        Validate.notEmpty(xmlContent, "xmlContent");

        // Build a DOM model of the provided xmlFileStream.
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);

        try {
            return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xmlContent)));
        } catch (Exception e) {
            throw new IllegalArgumentException("Could not create DOM Document", e);
        }
    }

    /**
     * Drives the supplied visitor to process the provided Node and all its children, should the recurseToChildren flag
     * be set to <code>true</code>. All attributes of the current node are processed before recursing to children (i.e.
     * breadth first recursion).
     *
     * @param node              The Node to process.
     * @param recurseToChildren if <code>true</code>, processes all children of the supplied node recursively.
     * @param visitor           The NodeProcessor instance which should process the nodes.
     */
    public final void process(final Node node, final boolean recurseToChildren, final NodeProcessor visitor) {

        // Process the current Node, if the NodeProcessor accepts it.
        if (visitor.accept(node)) {
            onAcceptedNode(node);
            visitor.process(node);
        }

        NamedNodeMap attributes = node.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
            Node attribute = attributes.item(i);

            // Process the current attribute, if the NodeProcessor accepts it.
            if (visitor.accept(attribute)) {
                onAcceptedAttribute(attribute);
                visitor.process(attribute);
            }
        }

        if (recurseToChildren) {
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node child = children.item(i);

                // Recurse to Element children.
                if (child.getNodeType() == Node.ELEMENT_NODE) {
                    process(child, true, visitor);
                }
            }
        }
    }

    /**
     * Event callback when a nodeProcessor has accepted a Node.
     *
     * @param aNode the accepted Node
     */
    protected void onAcceptedNode(final Node aNode) {
        // name="firstName"

        final Node nameAttribute = aNode.getAttributes().getNamedItem("name");
        if(nameAttribute != null) {

            final String nodeName = nameAttribute.getNodeValue();
            log.info("Accepted node [" + aNode.getNodeName() + "] " + nodeName);
        }
    }

    /**
     * Event callback when a nodeProcessor has accepted an Attribute.
     *
     * @param anAttribute the accepted attribute.
     */
    protected void onAcceptedAttribute(final Node anAttribute) {
        log.info("Accepted attribute [" + anAttribute.getNodeName() + "]");
    }

    //
    // Private helpers
    //

    /**
     * Utility method to read all (string formatted) data from the given classpath-relative
     * file and return the data as a string.
     *
     * @param path The classpath-relative file path.
     * @return The content of the supplied file.
     */
    protected static String readFully(final String path) {

        final StringBuilder toReturn = new StringBuilder(50);

        try {

            // Will produce a NPE if the path was not directed to a file.
            final InputStream resource = AbstractSourceCodeAwareNodeProcessingTest
                    .class
                    .getClassLoader()
                    .getResourceAsStream(path);
            final BufferedReader tmp = new BufferedReader(new InputStreamReader(resource));

            for (String line = tmp.readLine(); line != null; line = tmp.readLine()) {
                toReturn.append(line).append(AbstractJaxbMojo.NEWLINE);
            }
        } catch (final Exception e) {
            throw new IllegalArgumentException("Resource [" + path + "] not readable.");
        }

        // All done.
        return toReturn.toString();
    }

    /**
     * Compares XML documents provided by the two Readers.
     *
     * @param expected The expected document data.
     * @param actual   The actual document data.
     * @return A DetailedDiff object, describing all differences in documents supplied.
     * @throws org.xml.sax.SAXException If a SAXException was raised during parsing of the two Documents.
     * @throws IOException              If an I/O-related exception was raised while acquiring the data from the Readers.
     */
    protected static Diff compareXmlIgnoringWhitespace(final String expected, final String actual) throws SAXException,
            IOException {

        // Check sanity
        Validate.notNull(expected, "Cannot handle null expected argument.");
        Validate.notNull(actual, "Cannot handle null actual argument.");

        // Ignore whitespace - and also normalize the Documents.
        XMLUnit.setNormalize(true);
        XMLUnit.setIgnoreWhitespace(true);
        XMLUnit.setNormalize(true);

        // Compare and return
        return XMLUnit.compareXML(expected, actual);
    }

    private List<File> resolveSourceFiles() {

        final List<File> sourceDirs = Arrays.<File>asList(new File(basedir, "src/main/java"), testJavaDir);
        final List<File> candidates = FileSystemUtilities.resolveRecursively(sourceDirs, null, log);
        final List<File> toReturn = new ArrayList<File>();

        for (File current : candidates) {
            for (Class<?> currentClass : jaxbClasses) {

                final String expectedFileName = currentClass.getSimpleName() + ".java";
                if (expectedFileName.equalsIgnoreCase(current.getName())) {

                    final String transmutedCanonicalPath = FileSystemUtilities.getCanonicalPath(current)
                            .replace("/", ".")
                            .replace(File.separator, ".");

                    if (transmutedCanonicalPath.contains(currentClass.getPackage().getName())) {
                        toReturn.add(current);
                    }
                }
            }
        }

        // All done.
        return toReturn;
    }

    /**
     * Prints the content of the supplied DOM Document as a string.
     *
     * @param doc A non-null DOM Document.
     * @return A String holding the pretty-printed version of the supplied doc.
     */
    public static String printDocument(final Document doc) {

        try {
            // Create the Unity-Transformer
            final TransformerFactory tf = TransformerFactory.newInstance();
            final Transformer transformer = tf.newTransformer();

            // Make it pretty print stuff.
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

            // Harvest the result, and return.
            final StringWriter out = new StringWriter();
            transformer.transform(new DOMSource(doc), new StreamResult(out));
            return out.toString();
        } catch (Exception e) {
            throw new IllegalArgumentException("Could not print document", e);
        }
    }
} public class JavaDocExtractor {

    /**
     * The default value given as the return value from some annotation classes whenever the attribute
     * has not been supplied within the codebase.
     */
    private static final String DEFAULT_VALUE = "##default";

    // Internal state
    private JavaProjectBuilder builder;
    private Log log;

    /**
     * Creates a JavaDocExtractor wrapping the supplied Maven Log.
     *
     * @param log A non-null Log.
     */
    public JavaDocExtractor(final Log log) {

        // Check sanity
        Validate.notNull(log, "log");

        // Create internal state
        this.log = log;
        this.builder = new JavaProjectBuilder();
    }

    /**
     * Assigns the encoding of the underlying {@link JavaProjectBuilder}.
     *
     * @param encoding The non-empty encoding to be set into the underlying {@link JavaProjectBuilder}.
     */
    public void setEncoding(final String encoding) {
        this.builder.setEncoding(encoding);
    }

    /**
     * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor.
     *
     * @param sourceCodeFiles The non-null List of source code files to add.
     * @return This JavaDocExtractor, for call chaining.
     * @throws IllegalArgumentException If any of the given sourceCodeFiles could not be read properly.
     */
    public JavaDocExtractor addSourceFiles(final List<File> sourceCodeFiles) throws IllegalArgumentException {

        // Check sanity
        Validate.notNull(sourceCodeFiles, "addSourceFiles");

        // Add the files.
        for (File current : sourceCodeFiles) {
            try {
                builder.addSource(current);
            } catch (IOException e) {
                throw new IllegalArgumentException("Could not add file ["
                        + FileSystemUtilities.getCanonicalPath(current) + "]", e);
            }
        }

        // All done.
        return this;
    }

    /**
     * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor.
     *
     * @param sourceCodeURLs The non-null List of source code URLs to add.
     * @return This JavaDocExtractor, for call chaining.
     * @throws IllegalArgumentException If any of the given sourceCodeURLs could not be read properly.
     */
    public JavaDocExtractor addSourceURLs(final List<URL> sourceCodeURLs) throws IllegalArgumentException {

        // Check sanity
        Validate.notNull(sourceCodeURLs, "sourceCodeURLs");

        // Add the URLs
        for (URL current : sourceCodeURLs) {
            try {
                builder.addSource(current);
            } catch (IOException e) {
                throw new IllegalArgumentException("Could not add URL [" + current.toString() + "]", e);
            }
        }

        // All done
        return this;
    }

    /**
     * Processes all supplied Java source Files and URLs to extract JavaDocData for all ClassLocations from which
     * JavaDoc has been collected.
     *
     * @return A SearchableDocumentation relating SortableLocations and their paths to harvested JavaDocData.
     */
    public SearchableDocumentation process() {

        // Start processing.
        final SortedMap<SortableLocation, JavaDocData> dataHolder = new TreeMap<SortableLocation, JavaDocData>();
        final Collection<JavaSource> sources = builder.getSources();

        if (log.isInfoEnabled()) {
            log.info("Processing [" + sources.size() + "] java sources.");
        }

        for (JavaSource current : sources) {

            // Add the package-level JavaDoc
            final JavaPackage currentPackage = current.getPackage();
            final String packageName = currentPackage.getName();
            addEntry(dataHolder, new PackageLocation(packageName), currentPackage);

            if (log.isDebugEnabled()) {
                log.debug("Added package-level JavaDoc for [" + packageName + "]");
            }

            for (JavaClass currentClass : current.getClasses()) {

                // Add the class-level JavaDoc
                final String simpleClassName = currentClass.getName();
                final String classXmlName = getAnnotationAttributeValueFrom(XmlType.class,
                        "name",
                        currentClass.getAnnotations());

                final ClassLocation classLocation = new ClassLocation(packageName, simpleClassName, classXmlName);
                addEntry(dataHolder, classLocation, currentClass);

                if (log.isDebugEnabled()) {
                    log.debug("Added class-level JavaDoc for [" + classLocation + "]");
                }

                for (JavaField currentField : currentClass.getFields()) {

                    final List<JavaAnnotation> currentFieldAnnotations = currentField.getAnnotations();
                    String annotatedXmlName = null;

                    //
                    // Is this field a collection, annotated with @XmlElementWrapper?
                    // If so, the documentation should pertain to the corresponding XML Sequence,
                    // rather than the individual XML elements.
                    //
                    if (hasAnnotation(XmlElementWrapper.class, currentFieldAnnotations)) {

                        // There are 2 cases here:
                        //
                        // 1: The XmlElementWrapper is named.
                        // ==================================
                        // @XmlElementWrapper(name = "foobar")
                        // @XmlElement(name = "aString")
                        // private List<String> strings;
                        //
                        // ==> annotatedXmlName == "foobar"
                        //
                        // 2: The XmlElementWrapper is not named.
                        // ======================================
                        // @XmlElementWrapper
                        // @XmlElement(name = "anInteger")
                        // private SortedSet<Integer> integerSet;
                        //
                        // ==> annotatedXmlName == "integerSet"
                        //
                        annotatedXmlName = getAnnotationAttributeValueFrom(
                                XmlElementWrapper.class,
                                "name",
                                currentFieldAnnotations);

                        if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) {
                            annotatedXmlName = currentField.getName();
                        }
                    }

                    // Find the XML name if provided within an annotation.
                    if (annotatedXmlName == null) {
                        annotatedXmlName = getAnnotationAttributeValueFrom(
                                XmlElement.class,
                                "name",
                                currentFieldAnnotations);
                    }

                    if (annotatedXmlName == null) {
                        annotatedXmlName = getAnnotationAttributeValueFrom(
                                XmlAttribute.class,
                                "name",
                                currentFieldAnnotations);
                    }
                    if (annotatedXmlName == null) {
                        annotatedXmlName = getAnnotationAttributeValueFrom(
                                XmlEnumValue.class,
                                "value",
                                currentFieldAnnotations);
                    }

                    // Add the field-level JavaDoc
                    final FieldLocation fieldLocation = new FieldLocation(
                            packageName,
                            simpleClassName,
                            classXmlName,
                            currentField.getName(),
                            annotatedXmlName);

                    addEntry(dataHolder, fieldLocation, currentField);

                    if (log.isDebugEnabled()) {
                        log.debug("Added field-level JavaDoc for [" + fieldLocation + "]");
                    }
                }

                for (JavaMethod currentMethod : currentClass.getMethods()) {

                    final List<JavaAnnotation> currentMethodAnnotations = currentMethod.getAnnotations();
                    String annotatedXmlName = null;

                    //
                    // Is this field a collection, annotated with @XmlElementWrapper?
                    // If so, the documentation should pertain to the corresponding XML Sequence,
                    // rather than the individual XML elements.
                    //
                    if (hasAnnotation(XmlElementWrapper.class, currentMethodAnnotations)) {

                        // There are 2 cases here:
                        //
                        // 1: The XmlElementWrapper is named.
                        // ==================================
                        // @XmlElementWrapper(name = "foobar")
                        // @XmlElement(name = "aString")
                        // public List<String> getStrings() { ... };
                        //
                        // ==> annotatedXmlName == "foobar"
                        //
                        // 2: The XmlElementWrapper is not named.
                        // ======================================
                        // @XmlElementWrapper
                        // @XmlElement(name = "anInteger")
                        // public SortedSet<Integer> getIntegerSet() { ... };
                        //
                        // ==> annotatedXmlName == "getIntegerSet"
                        //
                        annotatedXmlName = getAnnotationAttributeValueFrom(
                                XmlElementWrapper.class,
                                "name",
                                currentMethodAnnotations);

                        if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) {
                            annotatedXmlName = currentMethod.getName();
                        }
                    }


                    // Find the XML name if provided within an annotation.
                    if (annotatedXmlName == null) {
                        annotatedXmlName = getAnnotationAttributeValueFrom(
                                XmlElement.class,
                                "name",
                                currentMethod.getAnnotations());
                    }

                    if (annotatedXmlName == null) {
                        annotatedXmlName = getAnnotationAttributeValueFrom(
                                XmlAttribute.class,
                                "name",
                                currentMethod.getAnnotations());
                    }

                    // Add the method-level JavaDoc
                    final MethodLocation location = new MethodLocation(packageName,
                            simpleClassName,
                            classXmlName,
                            currentMethod.getName(),
                            annotatedXmlName,
                            currentMethod.getParameters());
                    addEntry(dataHolder, location, currentMethod);

                    if (log.isDebugEnabled()) {
                        log.debug("Added method-level JavaDoc for [" + location + "]");
                    }
                }
            }
        }

        // All done.
        return new ReadOnlySearchableDocumentation(dataHolder);
    }

    /**
     * Finds the value of the attribute with the supplied name within the first matching JavaAnnotation of
     * the given type encountered in the given annotations List. This is typically used for reading values of
     * annotations such as {@link XmlElement}, {@link XmlAttribute} or {@link XmlEnumValue}.
     *
     * @param annotations    The list of JavaAnnotations to filter from.
     * @param annotationType The type of annotation to read attribute values from.
     * @param attributeName  The name of the attribute the value of which should be returned.
     * @return The first matching JavaAnnotation of type annotationType within the given annotations
     * List, or {@code null} if none was found.
     * @since 2.2
     */
    private static String getAnnotationAttributeValueFrom(
            final Class<?> annotationType,
            final String attributeName,
            final List<JavaAnnotation> annotations) {

        // QDox uses the fully qualified class name of the annotation for comparison.
        // Extract it.
        final String fullyQualifiedClassName = annotationType.getName();

        JavaAnnotation annotation = null;
        String toReturn = null;

        if (annotations != null) {

            for (JavaAnnotation current : annotations) {
                if (current.getType().isA(fullyQualifiedClassName)) {
                    annotation = current;
                    break;
                }
            }

            if (annotation != null) {

                final Object nameValue = annotation.getNamedParameter(attributeName);

                if (nameValue != null && nameValue instanceof String) {

                    toReturn = ((String) nameValue).trim();

                    // Remove initial and trailing " chars, if present.
                    if (toReturn.startsWith("\"") && toReturn.endsWith("\"")) {
                        toReturn = (((String) nameValue).trim()).substring(1, toReturn.length() - 1);
                    }
                }
            }
        }

        // All Done.
        return toReturn;
    }

    private static boolean hasAnnotation(final Class<?> annotationType,
                                         final List<JavaAnnotation> annotations) {

        if (annotations != null && !annotations.isEmpty() && annotationType != null) {

            final String fullAnnotationClassName = annotationType.getName();

            for (JavaAnnotation current : annotations) {
                if (current.getType().isA(fullAnnotationClassName)) {
                    return true;
                }
            }
        }

        return false;
    }

    //
    // Private helpers
    //

    private void addEntry(final SortedMap<SortableLocation, JavaDocData> map,
                          final SortableLocation key,
                          final JavaAnnotatedElement value) {

        // Check sanity
        if (map.containsKey(key)) {

            // Get something to compare with
            final JavaDocData existing = map.get(key);

            // Is this an empty package-level documentation?
            if (key instanceof PackageLocation) {

                final boolean emptyExisting = existing.getComment() == null || existing.getComment().isEmpty();
                final boolean emptyGiven = value.getComment() == null || value.getComment().isEmpty();

                if (emptyGiven) {
                    if (log.isDebugEnabled()) {
                        log.debug("Skipping processing empty Package javadoc from [" + key + "]");
                    }
                    return;
                } else if (emptyExisting && log.isWarnEnabled()) {
                    log.warn("Overwriting empty Package javadoc from [" + key + "]");
                }
            } else {
                final String given = "[" + value.getClass().getName() + "]: " + value.getComment();
                throw new IllegalArgumentException("Not processing duplicate SortableLocation [" + key + "]. "
                        + "\n Existing: " + existing
                        + ".\n Given: [" + given + "]");
            }
        }

        // Validate.isTrue(!map.containsKey(key), "Found duplicate SortableLocation [" + key + "] in map. "
        //         + "Current map keySet: " + map.keySet() + ". Got comment: [" + value.getComment() + "]");

        map.put(key, new JavaDocData(value.getComment(), value.getTags()));
    }

    /**
     * Standard read-only SearchableDocumentation implementation.
     */
    private class ReadOnlySearchableDocumentation implements SearchableDocumentation {

        // Internal state
        private TreeMap<String, SortableLocation> keyMap;
        private SortedMap<? extends SortableLocation, JavaDocData> valueMap;

        ReadOnlySearchableDocumentation(final SortedMap<SortableLocation, JavaDocData> valueMap) {

            // Create internal state
            this.valueMap = valueMap;

            keyMap = new TreeMap<String, SortableLocation>();
            for (Map.Entry<SortableLocation, JavaDocData> current : valueMap.entrySet()) {

                final SortableLocation key = current.getKey();
                keyMap.put(key.getPath(), key);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public SortedSet<String> getPaths() {
            return Collections.unmodifiableSortedSet(keyMap.navigableKeySet());
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public JavaDocData getJavaDoc(final String path) {

            // Check sanity
            Validate.notNull(path, "path");

            // All done.
            final SortableLocation location = getLocation(path);
            return (location == null) ? null : valueMap.get(location);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        @SuppressWarnings("unchecked")
        public <T extends SortableLocation> T getLocation(final String path) {

            // Check sanity
            Validate.notNull(path, "path");

            // All done
            return (T) keyMap.get(path);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        @SuppressWarnings("unchecked")
        public SortedMap<SortableLocation, JavaDocData> getAll() {
            return (SortedMap<SortableLocation, JavaDocData>) Collections.unmodifiableSortedMap(valueMap);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        @SuppressWarnings("unchecked")
        public <T extends SortableLocation> SortedMap<T, JavaDocData> getAll(final Class<T> type) {

            // Check sanity
            Validate.notNull(type, "type");

            // Filter the valueMap.
            final SortedMap<T, JavaDocData> toReturn = new TreeMap<T, JavaDocData>();
            for (Map.Entry<? extends SortableLocation, JavaDocData> current : valueMap.entrySet()) {
                if (type == current.getKey().getClass()) {
                    toReturn.put((T) current.getKey(), current.getValue());
                }
            }

            // All done.
            return toReturn;
        }
    }
} public class NoAuthorJavaDocRenderer extends DefaultJavaDocRenderer {

    /**
     * The author key.
     */
    private static final String AUTHOR_KEY = "author";

    /**
     * {@inheritDoc}
     */
    @Override
    protected String renderJavaDocTag(final String name, final String value, final SortableLocation location) {

        // Don't render the author
        if(AUTHOR_KEY.equalsIgnoreCase(name)) {
            return "";
        }

        // Delegate.
        return super.renderJavaDocTag(name, value, location);
    }
} public class XsdAnnotationProcessor implements NodeProcessor {

    // Internal state
    private SortedMap<ClassLocation, JavaDocData> classJavaDocs;
    private SortedMap<FieldLocation, JavaDocData> fieldJavaDocs;
    private SortedMap<MethodLocation, JavaDocData> methodJavaDocs;
    private JavaDocRenderer renderer;

    /**
     * Creates an XsdAnnotationProcessor that uses the supplied/generated SearchableDocumentation to read all
     * JavaDoc structures and the supplied JavaDocRenderer to render JavaDocs into XSD documentation annotations.
     *
     * @param docs     A non-null SearchableDocumentation, produced from the source code of the JAXB compilation unit.
     * @param renderer A non-null JavaDocRenderer, used to render the JavaDocData within the SearchableDocumentation.
     */
    public XsdAnnotationProcessor(final SearchableDocumentation docs, final JavaDocRenderer renderer) {

        // Check sanity
        Validate.notNull(docs, "docs");
        Validate.notNull(renderer, "renderer");

        // Assign internal state
        this.classJavaDocs = docs.getAll(ClassLocation.class);
        this.fieldJavaDocs = docs.getAll(FieldLocation.class);
        this.methodJavaDocs = docs.getAll(MethodLocation.class);
        this.renderer = renderer;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean accept(final Node aNode) {

        // Only deal with Element nodes with "name" attributes.
        if (!DomHelper.isNamedElement(aNode)) {
            return false;
        }

        /*
        <xs:complexType name="somewhatNamedPerson">
            <!-- ClassLocation JavaDocData insertion point -->

            <xs:sequence>

                <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
                <xs:element name="firstName" type="xs:string" nillable="true" minOccurs="0"/>

                <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
                <xs:element name="lastName" type="xs:string"/>
            </xs:sequence>

            <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
            <xs:attribute name="age" type="xs:int" use="required"/>
        </xs:complexType>
        */

        // Only process nodes corresponding to Types we have any JavaDoc for.
        // TODO: How should we handle PackageLocations and package documentation?

        boolean toReturn = false;
        if (DomHelper.getMethodLocation(aNode, methodJavaDocs.keySet()) != null) {
            toReturn = true;
        } else if (DomHelper.getFieldLocation(aNode, fieldJavaDocs.keySet()) != null) {
            toReturn = true;
        } else if (DomHelper.getClassLocation(aNode, classJavaDocs.keySet()) != null) {
            toReturn = true;
        }

        // All done.
        return toReturn;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void process(final Node aNode) {
        DomHelper.insertXmlDocumentationAnnotationsFor(aNode, classJavaDocs, fieldJavaDocs, methodJavaDocs, renderer);
    }
} public class XsdEnumerationAnnotationProcessor implements NodeProcessor {

    // Internal state
    private SortedMap<ClassLocation, JavaDocData> classJavaDocs;
    private SortedMap<FieldLocation, JavaDocData> fieldJavaDocs;
    private SortedMap<MethodLocation, JavaDocData> methodJavaDocs;
    private JavaDocRenderer renderer;

    /**
     * Creates an XsdEnumerationAnnotationProcessor that uses the supplied/generated SearchableDocumentation to read all
     * JavaDoc structures and the supplied JavaDocRenderer to render JavaDocs into XSD documentation annotations.
     *
     * @param docs     A non-null SearchableDocumentation, produced from the source code of the JAXB compilation unit.
     * @param renderer A non-null JavaDocRenderer, used to render the JavaDocData within the SearchableDocumentation.
     */
    public XsdEnumerationAnnotationProcessor(final SearchableDocumentation docs, final JavaDocRenderer renderer) {

        // Check sanity
        Validate.notNull(docs, "docs");
        Validate.notNull(renderer, "renderer");

        // Assign internal state
        this.classJavaDocs = docs.getAll(ClassLocation.class);
        this.fieldJavaDocs = docs.getAll(FieldLocation.class);
        this.methodJavaDocs = docs.getAll(MethodLocation.class);
        this.renderer = renderer;
    }

    /**
     * <p>Only accept simpleTypes which are restrictions to either <code>xs:string</code> or <code>xs:integer</code>.
     * The former is generated by JAXB when the Java Enum uses String values, and the latter is used
     * for ordinal values.</p>
     *
     * {@inheritDoc}
     */
    @Override
    public boolean accept(final Node aNode) {

        // Only deal with Element nodes.
        if (aNode.getNodeType() != Node.ELEMENT_NODE) {
            return false;
        }

        // We should accept:
        //
        // 1) A simpleType Element
        // 2) An enumeration Element
        final Element theElement = (Element) aNode;

        final String localName = theElement.getLocalName();
        if (localName != null) {

            final String trimmed = localName.trim();
            return trimmed.equalsIgnoreCase("enumeration")
                    || trimmed.equalsIgnoreCase("simpleType");
        }

        /*
        <xs:simpleType name="foodPreference">
            <!-- ClassLocation JavaDocData insertion point -->

            <xs:restriction base="xs:string">

                <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
                <xs:enumeration value="NONE"/>

                <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
                <xs:enumeration value="LACTO_VEGETARIAN"/>

                <!-- FieldLocation or MethodLocation JavaDocData insertion point (within child) -->
                <xs:enumeration value="VEGAN"/>

            </xs:restriction>
        </xs:simpleType>
         */

        /*
  <xs:simpleType name="foodPreference">
    <xs:restriction base="xs:string">
      <xs:enumeration value="NONE"/>
      <xs:enumeration value="LACTO_VEGETARIAN"/>
      <xs:enumeration value="VEGAN"/>
    </xs:restriction>
  </xs:simpleType>

  <xs:simpleType name="americanCoin">
    <xs:restriction base="xs:int">
      <xs:enumeration value="25"/>
      <xs:enumeration value="1"/>
      <xs:enumeration value="5"/>
      <xs:enumeration value="10"/>
    </xs:restriction>
  </xs:simpleType>
        */

        // All done.
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void process(final Node aNode) {
        DomHelper.insertXmlDocumentationAnnotationsFor(aNode, classJavaDocs, fieldJavaDocs, methodJavaDocs, renderer);
    }
} public class SimpleNamespaceResolver implements NamespaceContext {

    // Constants
    private static final String DEFAULT_NS = "DEFAULT";
    private static final String TARGET_NAMESPACE = "targetNamespace";
    private static final String SCHEMA = "schema";

    // Internal state
    private String sourceFilename;
    private String localNamespaceURI;
    private Map<String, String> prefix2Uri = new HashMap<String, String>();
    private Map<String, String> uri2Prefix = new HashMap<String, String>();

    /**
     * Creates a new SimpleNamespaceResolver which collects namespace data
     * from the provided XML file.
     *
     * @param xmlFile The XML file from which to collect namespace data, should not be null.
     */
    public SimpleNamespaceResolver(final File xmlFile) {
        this.sourceFilename = xmlFile.getName();

        Reader reader = null;
        try {
            reader = new FileReader(xmlFile);
            initialize(reader);
        } catch (FileNotFoundException e) {
            throw new IllegalArgumentException("File [" + xmlFile + "] could not be found.");
        } finally {
            IOUtil.close(reader);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getNamespaceURI(final String prefix) {
        if (prefix == null) {
            // Be compliant with the JAXB contract for NamespaceResolver.
            throw new IllegalArgumentException("Cannot handle null prefix argument.");
        }

        return prefix2Uri.get(XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) ? DEFAULT_NS : prefix);
    }

    /**
     * {@inheritDoc}
     */
    public String getPrefix(final String namespaceURI) {
        if (namespaceURI == null) {
            // Be compliant with the JAXB contract for NamespaceResolver.
            throw new IllegalArgumentException("Cannot acquire prefix for null namespaceURI.");
        }

        return uri2Prefix.get(namespaceURI);
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<String> getPrefixes(final String namespaceURI) {
        if (namespaceURI == null) {
            // Be compliant with the JAXB contract for NamespaceResolver.
            throw new IllegalArgumentException("Cannot acquire prefixes for null namespaceURI.");
        }

        return Collections.singletonList(uri2Prefix.get(namespaceURI)).iterator();
    }

    /**
     * @return A readonly map relating namespace URIs to namespace prefixes.
     */
    public Map<String, String> getNamespaceURI2PrefixMap() {
        return Collections.unmodifiableMap(uri2Prefix);
    }

    /**
     * @return The namespace URI of the default namespace within the sourceFile of this SimpleNamespaceResolver.
     */
    public String getLocalNamespaceURI() {
        return localNamespaceURI;
    }

    /**
     * @return The name of the source file used for this SimpleNamespaceResolver.
     */
    public String getSourceFilename() {
        return sourceFilename;
    }

    //
    // Private helpers
    //

    /**
     * Initializes this SimpleNamespaceResolver to collect namespace data from the provided stream.
     *
     * @param xmlFileStream A Reader connected to the XML file from which we should read namespace data.
     */
    private void initialize(final Reader xmlFileStream) {

        // Build a DOM model.
        final Document parsedDocument = XsdGeneratorHelper.parseXmlStream(xmlFileStream);

        // Process the DOM model.
        XsdGeneratorHelper.process(parsedDocument.getFirstChild(), true, new NamespaceAttributeNodeProcessor());
    }

    private class NamespaceAttributeNodeProcessor
            implements NodeProcessor {
        /**
         * Defines if this visitor should process the provided node.
         *
         * @param aNode The DOM node to process.
         * @return <code>true</code> if the provided Node should be processed by this NodeProcessor.
         */
        public boolean accept(final Node aNode) {

            // Correct namespace?
            if (aNode.getNamespaceURI() != null
                    && XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(aNode.getNamespaceURI())) {
                return true;
            }

            // Is this Node the targetNamespace attribute?
            if (aNode instanceof Attr) {

                final Attr attribute = (Attr) aNode;
                final Element parent = attribute.getOwnerElement();
                if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(parent.getNamespaceURI())
                        && SCHEMA.equalsIgnoreCase(parent.getLocalName())
                        && TARGET_NAMESPACE.equals(attribute.getLocalName())) {

                    SimpleNamespaceResolver.this.localNamespaceURI = attribute.getNodeValue();
                }
            }

            // Ignore processing this Node.
            return false;
        }

        /**
         * Processes the provided DOM Node.
         *
         * @param aNode The DOM Node to process.
         */
        public void process(final Node aNode) {

            // If we have no namespace, use the DEFAULT_NS as the prefix
            final String cacheKey = XMLConstants.XMLNS_ATTRIBUTE.equals(aNode.getNodeName())
                    ? DEFAULT_NS
                    : aNode.getLocalName();
            final String nodeValue = aNode.getNodeValue();

            // Cache the namespace in both caches.
            final String oldUriValue = prefix2Uri.put(cacheKey, nodeValue);
            final String oldPrefixValue = uri2Prefix.put(nodeValue, cacheKey);

            // Check sanity; we should not be overwriting values here.
            if (oldUriValue != null) {
                throw new IllegalStateException(
                        "Replaced URI [" + oldUriValue + "] with [" + aNode.getNodeValue() + "] for prefix [" + cacheKey
                                + "]");
            }
            if (oldPrefixValue != null) {
                throw new IllegalStateException(
                        "Replaced prefix [" + oldPrefixValue + "] with [" + cacheKey + "] for URI [" + aNode.getNodeValue()
                                + "]");
            }
        }
    }
} public final class FileSystemUtilities {

    /*
     * Hide the constructor for utility classes.
     */
    private FileSystemUtilities() {
        // Do nothing
    }

    /**
     * FileFilter which accepts Files that exist and for which {@code File.isFile() } is {@code true}.
     */
    public static final FileFilter EXISTING_FILE = new FileFilter() {
        @Override
        public boolean accept(final File candidate) {
            return candidate != null && candidate.exists() && candidate.isFile();
        }
    };

    /**
     * FileFilter which accepts Files that exist and for which {@code File.isDirectory() } is {@code true}.
     */
    public static final FileFilter EXISTING_DIRECTORY = new FileFilter() {
        @Override
        public boolean accept(final File candidate) {
            return candidate != null && candidate.exists() && candidate.isDirectory();
        }
    };

    /**
     * Acquires the canonical path for the supplied file.
     *
     * @param file A non-null File for which the canonical path should be retrieved.
     * @return The canonical path of the supplied file.
     */
    public static String getCanonicalPath(final File file) {
        return getCanonicalFile(file).getPath();
    }

    /**
     * Non-valid Characters for naming files, folders under Windows: <code>":", "*", "?", "\"", "<", ">", "|"</code>
     *
     * @see <a href="http://support.microsoft.com/?scid=kb%3Ben-us%3B177506&x=12&y=13">
     * http://support.microsoft.com/?scid=kb%3Ben-us%3B177506&x=12&y=13</a>
     * @see org.codehaus.plexus.util.FileUtils
     */
    private static final String[] INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME = {":", "*", "?", "\"", "<", ">", "|"};

    /**
     * Acquires the canonical File for the supplied file.
     *
     * @param file A non-null File for which the canonical File should be retrieved.
     * @return The canonical File of the supplied file.
     */
    public static File getCanonicalFile(final File file) {

        // Check sanity
        Validate.notNull(file, "file");

        // All done
        try {
            return file.getCanonicalFile();
        } catch (IOException e) {
            throw new IllegalArgumentException("Could not acquire the canonical file for ["
                    + file.getAbsolutePath() + "]", e);
        }
    }

    /**
     * <p>Retrieves the canonical File matching the supplied path in the following order or priority:</p>
     * <ol>
     * <li><strong>Absolute path:</strong> The path is used by itself (i.e. {@code new File(path);}). If an
     * existing file or directory matches the provided path argument, its canonical path will be returned.</li>
     * <li><strong>Relative path:</strong> The path is appended to the baseDir (i.e.
     * {@code new File(baseDir, path);}). If an existing file or directory matches the provided path argument,
     * its canonical path will be returned. Only in this case will be baseDir argument be considered.</li>
     * </ol>
     * <p>If no file or directory could be derived from the supplied path and baseDir, {@code null} is returned.</p>
     *
     * @param path    A non-null path which will be used to find an existing file or directory.
     * @param baseDir A directory to which the path will be appended to search for the existing file or directory in
     *                case the file was nonexistent when interpreted as an absolute path.
     * @return either a canonical File for the path, or {@code null} if no file or directory matched
     * the supplied path and baseDir.
     */
    public static File getExistingFile(final String path, final File baseDir) {

        // Check sanity
        Validate.notEmpty(path, "path");
        final File theFile = new File(path);
        File toReturn = null;

        // Is 'path' absolute?
        if (theFile.isAbsolute() && (EXISTING_FILE.accept(theFile) || EXISTING_DIRECTORY.accept(theFile))) {
            toReturn = getCanonicalFile(theFile);
        }

        // Is 'path' relative?
        if (!theFile.isAbsolute()) {

            // In this case, baseDir cannot be null.
            Validate.notNull(baseDir, "baseDir");
            final File relativeFile = new File(baseDir, path);

            if (EXISTING_FILE.accept(relativeFile) || EXISTING_DIRECTORY.accept(relativeFile)) {
                toReturn = getCanonicalFile(relativeFile);
            }
        }

        // The path provided did not point to an existing File or Directory.
        return toReturn;
    }

    /**
     * Retrieves the URL for the supplied File. Convenience method which hides exception handling
     * for the operation in question.
     *
     * @param aFile A File for which the URL should be retrieved.
     * @return The URL for the supplied aFile.
     * @throws java.lang.IllegalArgumentException if getting the URL yielded a MalformedURLException.
     */
    public static URL getUrlFor(final File aFile) throws IllegalArgumentException {

        // Check sanity
        Validate.notNull(aFile, "aFile");

        try {
            return aFile.toURI().normalize().toURL();
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Could not retrieve the URL from file ["
                    + getCanonicalPath(aFile) + "]", e);
        }
    }

    /**
     * Acquires the file for a supplied URL, provided that its protocol is is either a file or a jar.
     *
     * @param anURL    a non-null URL.
     * @param encoding The encoding to be used by the URLDecoder to decode the path found.
     * @return The File pointing to the supplied URL, for file or jar protocol URLs and null otherwise.
     */
    public static File getFileFor(final URL anURL, final String encoding) {

        // Check sanity
        Validate.notNull(anURL, "anURL");
        Validate.notNull(encoding, "encoding");

        final String protocol = anURL.getProtocol();
        File toReturn = null;
        if ("file".equalsIgnoreCase(protocol)) {
            try {
                final String decodedPath = URLDecoder.decode(anURL.getPath(), encoding);
                toReturn = new File(decodedPath);
            } catch (Exception e) {
                throw new IllegalArgumentException("Could not get the File for [" + anURL + "]", e);
            }
        } else if ("jar".equalsIgnoreCase(protocol)) {

            try {

                // Decode the JAR
                final String tmp = URLDecoder.decode(anURL.getFile(), encoding);

                // JAR URLs generally contain layered protocols, such as:
                // jar:file:/some/path/to/nazgul-tools-validation-aspect-4.0.2.jar!/the/package/ValidationAspect.class
                final URL innerURL = new URL(tmp);

                // We can handle File protocol URLs here.
                if ("file".equalsIgnoreCase(innerURL.getProtocol())) {

                    // Peel off the inner protocol
                    final String innerUrlPath = innerURL.getPath();
                    final String filePath = innerUrlPath.contains("!")
                            ? innerUrlPath.substring(0, innerUrlPath.indexOf("!"))
                            : innerUrlPath;
                    toReturn = new File(URLDecoder.decode(filePath, encoding));
                }
            } catch (Exception e) {
                throw new IllegalArgumentException("Could not get the File for [" + anURL + "]", e);
            }
        }

        // All done.
        return toReturn;
    }


    /**
     * Filters files found either in the sources paths (or in the standardDirectory if no explicit sources are given),
     * and retrieves a List holding those files that do not match any of the supplied Java Regular Expression
     * excludePatterns.
     *
     * @param baseDir             The non-null basedir Directory.
     * @param sources             The sources which should be either absolute or relative (to the given baseDir)
     *                            paths to files or to directories that should be searched recursively for files.
     * @param standardDirectories If no sources are given, revert to searching all files under these standard
     *                            directories. Each path is {@code relativize()}-d to the supplied baseDir to
     *                            reach a directory path.
     * @param log                 A non-null Maven Log for logging any operations performed.
     * @param fileTypeDescription A human-readable short description of what kind of files are searched for, such as
     *                            "xsdSources" or "xjbSources".
     * @param excludePatterns     An optional List of patterns used to construct an ExclusionRegExpFileFilter used to
     *                            identify files which should be excluded from the result.
     * @return URLs to all Files under the supplied sources (or standardDirectories, if no explicit sources
     * are given) which do not match the supplied Java Regular excludePatterns.
     */
    @SuppressWarnings("all")
    public static List<URL> filterFiles(final File baseDir,
                                        final List<String> sources,
                                        final List<String> standardDirectories,
                                        final Log log,
                                        final String fileTypeDescription,
                                        final List<Filter<File>> excludePatterns) {

        final SortedMap<String, File> pathToResolvedSourceMap = new TreeMap<String, File>();

        for (String current : standardDirectories) {
            for (File currentResolvedSource : FileSystemUtilities.filterFiles(
                    baseDir,
                    sources,
                    FileSystemUtilities.relativize(current, baseDir, true),
                    log,
                    fileTypeDescription,
                    excludePatterns)) {

                // Add the source
                pathToResolvedSourceMap.put(
                        FileSystemUtilities.getCanonicalPath(currentResolvedSource),
                        currentResolvedSource);
            }
        }

        final List<URL> toReturn = new ArrayList<URL>();

        // Extract the URLs for all resolved Java sources.
        for (Map.Entry<String, File> current : pathToResolvedSourceMap.entrySet()) {
            toReturn.add(FileSystemUtilities.getUrlFor(current.getValue()));
        }

        if (log.isDebugEnabled()) {

            final StringBuilder builder = new StringBuilder();
            builder.append("\n+=================== [Filtered " + fileTypeDescription + "]\n");

            builder.append("|\n");
            builder.append("| " + excludePatterns.size() + " Exclude patterns:\n");
            for (int i = 0; i < excludePatterns.size(); i++) {
                builder.append("| [" + (i + 1) + "/" + excludePatterns.size() + "]: " + excludePatterns.get(i) + "\n");
            }

            builder.append("|\n");
            builder.append("| " + standardDirectories.size() + " Standard Directories:\n");
            for (int i = 0; i < standardDirectories.size(); i++) {
                builder.append("| [" + (i + 1) + "/" + standardDirectories.size() + "]: "
                        + standardDirectories.get(i) + "\n");
            }

            builder.append("|\n");
            builder.append("| " + toReturn.size() + " Results:\n");
            for (int i = 0; i < toReturn.size(); i++) {
                builder.append("| [" + (i + 1) + "/" + toReturn.size() + "]: " + toReturn.get(i) + "\n");
            }
            builder.append("|\n");
            builder.append("+=================== [End Filtered " + fileTypeDescription + "]\n\n");

            // Log all.
            log.debug(builder.toString().replace("\n", AbstractJaxbMojo.NEWLINE));
        }

        // All done.
        return toReturn;
    }

    /**
     * Filters files found either in the sources paths (or in the standardDirectory if no explicit sources are given),
     * and retrieves a List holding those files that do not match any of the supplied Java Regular Expression
     * excludePatterns.
     *
     * @param baseDir             The non-null basedir Directory.
     * @param sources             The sources which should be either absolute or relative (to the given baseDir)
     *                            paths to files or to directories that should be searched recursively for files.
     * @param standardDirectory   If no sources are given, revert to searching all files under this standard directory.
     *                            This is the path appended to the baseDir to reach a directory.
     * @param log                 A non-null Maven Log for logging any operations performed.
     * @param fileTypeDescription A human-readable short description of what kind of files are searched for, such as
     *                            "xsdSources" or "xjbSources".
     * @param excludeFilters      An optional List of Filters used to identify files which should be excluded from
     *                            the result.
     * @return All files under the supplied sources (or standardDirectory, if no explicit sources are given) which
     * do not match the supplied Java Regular excludePatterns.
     */
    @SuppressWarnings("CheckStyle")
    public static List<File> filterFiles(final File baseDir,
                                         final List<String> sources,
                                         final String standardDirectory,
                                         final Log log,
                                         final String fileTypeDescription,
                                         final List<Filter<File>> excludeFilters) {

        // Check sanity
        Validate.notNull(baseDir, "baseDir");
        Validate.notNull(log, "log");
        Validate.notEmpty(standardDirectory, "standardDirectory");
        Validate.notEmpty(fileTypeDescription, "fileTypeDescription");

        // No sources provided? Fallback to the standard (which should be a relative path).
        List<String> effectiveSources = sources;
        if (sources == null || sources.isEmpty()) {
            effectiveSources = new ArrayList<String>();

            final File tmp = new File(standardDirectory);
            final File rootDirectory = tmp.isAbsolute() ? tmp : new File(baseDir, standardDirectory);
            effectiveSources.add(FileSystemUtilities.getCanonicalPath(rootDirectory));
        }

        // First, remove the non-existent sources.
        List<File> existingSources = new ArrayList<File>();
        for (String current : effectiveSources) {

            final File existingFile = FileSystemUtilities.getExistingFile(current, baseDir);
            if (existingFile != null) {
                existingSources.add(existingFile);

                if (log.isDebugEnabled()) {
                    log.debug("Accepted configured " + fileTypeDescription + " ["
                            + FileSystemUtilities.getCanonicalFile(existingFile) + "]");
                }
            } else {
                if (log.isInfoEnabled()) {
                    log.info("Ignored given or default " + fileTypeDescription + " [" + current
                            + "], since it is not an existent file or directory.");
                }
            }
        }

        if (log.isDebugEnabled() && existingSources.size() > 0) {

            final int size = existingSources.size();

            log.debug(" [" + size + " existing " + fileTypeDescription + "] ...");
            for (int i = 0; i < size; i++) {
                log.debug("   " + (i + 1) + "/" + size + ": " + existingSources.get(i));
            }
            log.debug(" ... End [" + size + " existing " + fileTypeDescription + "]");
        }

        // All Done.
        return FileSystemUtilities.resolveRecursively(existingSources, excludeFilters, log);
    }

    /**
     * Filters all supplied files using the provided {@code acceptFilter}.
     *
     * @param files        The list of files to resolve, filter and return. If the {@code files} List
     *                     contains directories, they are searched for Files recursively. Any found Files in such
     *                     a search are included in the resulting File List if they match the acceptFilter supplied.
     * @param acceptFilter A filter matched to all files in the given List. If the acceptFilter matches a file, it is
     *                     included in the result.
     * @param log          The active Maven Log.
     * @return All files in (or files in subdirectories of directories provided in) the files List, provided that each
     * file is accepted by an ExclusionRegExpFileFilter.
     */
    public static List<File> filterFiles(final List<File> files, final Filter<File> acceptFilter, final Log log) {

        // Check sanity
        Validate.notNull(files, "files");

        final List<File> toReturn = new ArrayList<File>();

        if (files.size() > 0) {
            for (File current : files) {

                final boolean isAcceptedFile = EXISTING_FILE.accept(current) && acceptFilter.accept(current);
                final boolean isAcceptedDirectory = EXISTING_DIRECTORY.accept(current) && acceptFilter.accept(current);

                if (isAcceptedFile) {
                    toReturn.add(current);
                } else if (isAcceptedDirectory) {
                    recurseAndPopulate(toReturn, Collections.singletonList(acceptFilter), current, false, log);
                }
            }
        }

        // All done
        return toReturn;
    }

    /**
     * Retrieves a List of Files containing all the existing files within the supplied files List, including all
     * files found in directories recursive to any directories provided in the files list. Each file included in the
     * result must pass an ExclusionRegExpFileFilter synthesized from the supplied exclusions pattern(s).
     *
     * @param files            The list of files to resolve, filter and return. If the {@code files} List
     *                         contains directories, they are searched for Files recursively. Any found Files in such
     *                         a search are included in the resulting File List if they do not match any of the
     *                         exclusionFilters supplied.
     * @param exclusionFilters A List of Filters which identify files to remove from the result - implying that any
     *                         File matched by any of these exclusionFilters will not be included in the result.
     * @param log              The active Maven Log.
     * @return All files in (or files in subdirectories of directories provided in) the files List, provided that each
     * file is accepted by an ExclusionRegExpFileFilter.
     */
    public static List<File> resolveRecursively(final List<File> files,
                                                final List<Filter<File>> exclusionFilters,
                                                final Log log) {

        // Check sanity
        Validate.notNull(files, "files");

        final List<Filter<File>> effectiveExclusions = exclusionFilters == null
                ? new ArrayList<Filter<File>>()
                : exclusionFilters;

        final List<File> toReturn = new ArrayList<File>();

        if (files.size() > 0) {
            for (File current : files) {

                final boolean isAcceptedFile = EXISTING_FILE.accept(current)
                        && Filters.noFilterMatches(current, effectiveExclusions);
                final boolean isAcceptedDirectory = EXISTING_DIRECTORY.accept(current)
                        && Filters.noFilterMatches(current, effectiveExclusions);

                if (isAcceptedFile) {
                    toReturn.add(current);
                } else if (isAcceptedDirectory) {
                    recurseAndPopulate(toReturn, effectiveExclusions, current, true, log);
                }
            }
        }

        // All done
        return toReturn;
    }

    /**
     * Convenience method to successfully create a directory - or throw an exception if failing to create it.
     *
     * @param aDirectory        The directory to create.
     * @param cleanBeforeCreate if {@code true}, the directory and all its content will be deleted before being
     *                          re-created. This will ensure that the created directory is really clean.
     * @throws MojoExecutionException if the aDirectory could not be created (and/or cleaned).
     */
    public static void createDirectory(final File aDirectory, final boolean cleanBeforeCreate)
            throws MojoExecutionException {

        // Check sanity
        Validate.notNull(aDirectory, "aDirectory");
        validateFileOrDirectoryName(aDirectory);

        // Clean an existing directory?
        if (cleanBeforeCreate) {
            try {
                FileUtils.deleteDirectory(aDirectory);
            } catch (IOException e) {
                throw new MojoExecutionException("Could not clean directory [" + getCanonicalPath(aDirectory) + "]", e);
            }
        }

        // Now, make the required directory, if it does not already exist as a directory.
        final boolean existsAsFile = aDirectory.exists() && aDirectory.isFile();
        if (existsAsFile) {
            throw new MojoExecutionException("[" + getCanonicalPath(aDirectory) + "] exists and is a file. "
                    + "Cannot make directory");
        } else if (!aDirectory.exists() && !aDirectory.mkdirs()) {
            throw new MojoExecutionException("Could not create directory [" + getCanonicalPath(aDirectory) + "]");
        }
    }

    /**
     * If the supplied path refers to a file or directory below the supplied basedir, the returned
     * path is identical to the part below the basedir.
     *
     * @param path      The path to strip off basedir path from, and return.
     * @param parentDir The maven project basedir.
     * @param removeInitialFileSep If true, an initial {@code File#separator} is removed before returning.
     * @return The path relative to basedir, if it is situated below the basedir. Otherwise the supplied path.
     */
    public static String relativize(final String path,
                                    final File parentDir,
                                    final boolean removeInitialFileSep) {

        // Check sanity
        Validate.notNull(path, "path");
        Validate.notNull(parentDir, "parentDir");

        final Path p = Paths.get(path);
        final Path pd = parentDir.toPath();

        String platformSpecificPath;
        if (p.normalize().startsWith(pd.normalize().toString())) {
            platformSpecificPath = pd.relativize(p).toString();
        } else {
            platformSpecificPath = p.toString();
        }

        if (removeInitialFileSep && platformSpecificPath.startsWith(File.separator)) {
            platformSpecificPath = platformSpecificPath.substring(File.separator.length());
        }

        // NOTE: it appears this function is meant to preserve the file separator that was passed in the path
        if (path.indexOf('\\') == -1) {
            platformSpecificPath = platformSpecificPath.replace('\\', '/');
        }

        return platformSpecificPath;
    }

    /**
     * If the supplied fileOrDir is a File, it is added to the returned List if any of the filters Match.
     * If the supplied fileOrDir is a Directory, it is listed and any of the files immediately within the fileOrDir
     * directory are returned within the resulting List provided that they match any of the supplied filters.
     *
     * @param fileOrDir   A File or Directory.
     * @param fileFilters A List of filter of which at least one must match to add the File
     *                    (or child Files, in case of a directory) to the resulting List.
     * @param log         The active Maven Log
     * @return A List holding the supplied File (or child Files, in case fileOrDir is a Directory) given that at
     * least one Filter accepts them.
     */
    @SuppressWarnings("all")
    public static List<File> listFiles(final File fileOrDir,
                                       final List<Filter<File>> fileFilters,
                                       final Log log) {
        return listFiles(fileOrDir, fileFilters, false, log);
    }

    /**
     * If the supplied fileOrDir is a File, it is added to the returned List if any of the filters Match.
     * If the supplied fileOrDir is a Directory, it is listed and any of the files immediately within the fileOrDir
     * directory are returned within the resulting List provided that they match any of the supplied filters.
     *
     * @param fileOrDir              A File or Directory.
     * @param fileFilters            A List of filter of which at least one must match to add the File (or child Files, in case
     *                               of a directory) to the resulting List.
     * @param excludeFilterOperation if {@code true}, all fileFilters are considered exclude filter, i.e. if
     *                               any of the Filters match the fileOrDir, that fileOrDir will be excluded from the
     *                               result.
     * @param log                    The active Maven Log
     * @return A List holding the supplied File (or child Files, in case fileOrDir is a Directory) given that at
     * least one Filter accepts them.
     */
    @SuppressWarnings("all")
    public static List<File> listFiles(final File fileOrDir,
                                       final List<Filter<File>> fileFilters,
                                       final boolean excludeFilterOperation,
                                       final Log log) {

        // Check sanity
        Validate.notNull(log, "log");
        Validate.notNull(fileFilters, "fileFilters");
        final List<File> toReturn = new ArrayList<File>();

        if (EXISTING_FILE.accept(fileOrDir)) {
            checkAndAdd(toReturn, fileOrDir, fileFilters, excludeFilterOperation, log);
        } else if (EXISTING_DIRECTORY.accept(fileOrDir)) {

            final File[] listedFiles = fileOrDir.listFiles();

            if (listedFiles != null) {
                for (File current : listedFiles) {

                    // Typically, hidden directories start with '.'
                    // Except them from automagic filtering.
                    if (current.getName().charAt(0) != '.') {
                        checkAndAdd(toReturn, current, fileFilters, excludeFilterOperation, log);
                    }
                }
            }
        }

        // All done.
        return toReturn;
    }

    //
    // Private helpers
    //

    private static void checkAndAdd(final List<File> toPopulate,
                                    final File current,
                                    final List<Filter<File>> fileFilters,
                                    final boolean excludeFilterOperation,
                                    final Log log) {

        //
        // When no filters are supplied...
        // [Include Operation]: all files will be rejected
        // [Exclude Operation]: all files will be included
        //
        final boolean noFilters = fileFilters == null || fileFilters.isEmpty();
        final boolean addFile = excludeFilterOperation
                ? noFilters || Filters.rejectAtLeastOnce(current, fileFilters)
                : noFilters || Filters.matchAtLeastOnce(current, fileFilters);
        final String logPrefix = (addFile ? "Accepted " : "Rejected ")
                + (current.isDirectory() ? "directory" : "file") + " [";

        if (addFile) {
            toPopulate.add(current);
        }

        if (log.isDebugEnabled()) {
            log.debug(logPrefix + getCanonicalPath(current) + "]");
        }
    }

    private static void validateFileOrDirectoryName(final File fileOrDir) {

        if (Os.isFamily(Os.FAMILY_WINDOWS) && !FileUtils.isValidWindowsFileName(fileOrDir)) {
            throw new IllegalArgumentException(
                    "The file (" + fileOrDir + ") cannot contain any of the following characters: \n"
                            + StringUtils.join(INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME, " "));
        }
    }

    private static void recurseAndPopulate(final List<File> toPopulate,
                                           final List<Filter<File>> fileFilters,
                                           final File aDirectory,
                                           final boolean excludeOperation,
                                           final Log log) {

        final List<File> files = listFiles(aDirectory, fileFilters, excludeOperation, log);
        for (File current : files) {
            if (EXISTING_FILE.accept(current)) {
                toPopulate.add(current);
            }

            if (EXISTING_DIRECTORY.accept(current)) {
                recurseAndPopulate(toPopulate, fileFilters, current, excludeOperation, log);
            }
        }
    }
} public interface Filter<T> {

    /**
     * Initializes this Filter, and assigns the supplied Log for use by this Filter.
     *
     * @param log The non-null Log which should be used by this Filter to emit log messages.
     */
    void initialize(Log log);

    /**
     * @return {@code true} if this Filter has been properly initialized (by a call to the {@code initialize} method).
     */
    boolean isInitialized();

    /**
     * <p>Method that is invoked to determine if a candidate instance should be accepted or not.
     * Implementing classes should be prepared to handle {@code null} candidate objects.</p>
     *
     * @param candidate The candidate that should be tested for acceptance by this Filter.
     * @return {@code true} if the candidate is accepted by this Filter and {@code false} otherwise.
     * @throws java.lang.IllegalStateException if this Filter is not initialized by a call to the
     *                                         initialize method before calling this matchAtLeastOnce method.
     */
    boolean accept(T candidate) throws IllegalStateException;
} @SuppressWarnings("all")
public final class Filters {

    /**
     * Algorithms for accepting the supplied object if at least one of the supplied Filters accepts it.
     *
     * @param object  The object to accept (or not).
     * @param filters The non-null list of Filters to examine the supplied object.
     * @param <T>     The Filter type.
     * @return {@code true} if at least one of the filters return true from its accept method.
     * @see Filter#accept(Object)
     */
    public static <T> boolean matchAtLeastOnce(final T object, final List<Filter<T>> filters) {

        // Check sanity
        Validate.notNull(filters, "filters");

        boolean acceptedByAtLeastOneFilter = false;
        for (Filter<T> current : filters) {
            if (current.accept(object)) {
                acceptedByAtLeastOneFilter = true;
                break;
            }
        }

        // All done.
        return acceptedByAtLeastOneFilter;
    }

    /**
     * Algorithms for rejecting the supplied object if at least one of the supplied Filters does not accept it.
     *
     * @param object  The object to reject (or not).
     * @param filters The non-null list of Filters to examine the supplied object.
     * @param <T>     The Filter type.
     * @return {@code true} if at least one of the filters returns false from its accept method.
     * @see Filter#accept(Object)
     */
    public static <T> boolean rejectAtLeastOnce(final T object, final List<Filter<T>> filters) {

        // Check sanity
        Validate.notNull(filters, "filters");

        boolean rejectedByAtLeastOneFilter = false;
        for (Filter<T> current : filters) {
            if (!current.accept(object)) {
                rejectedByAtLeastOneFilter = true;
                break;
            }
        }

        // All done.
        return rejectedByAtLeastOneFilter;
    }

    /**
     * Algorithms for rejecting the supplied object if at least one of the supplied Filters rejects it.
     *
     * @param object  The object to accept (or not).
     * @param filters The non-null list of Filters to examine the supplied object.
     * @param <T>     The Filter type.
     * @return {@code true} if at least one of the filters return false from its accept method.
     * @see Filter#accept(Object)
     */
    public static <T> boolean noFilterMatches(final T object, final List<Filter<T>> filters) {

        // Check sanity
        Validate.notNull(filters, "filters");

        boolean matchedAtLeastOnce = false;
        for (Filter<T> current : filters) {
            if (current.accept(object)) {
                matchedAtLeastOnce = true;
            }
        }

        // All done.
        return !matchedAtLeastOnce;
    }

    /**
     * Adapts the Filter specification to the FileFilter interface, to enable immediate use
     * for filtering File lists.
     *
     * @param toAdapt The non-null Filter which should be adapted to a FileFilter interface.
     * @return If the {@code toAdapt} instance already implements the FileFilter interface, simply return the toAdapt
     * instance. Otherwise, returns a FileFilter interface which delegates its execution to the wrapped Filter.
     */
    public static FileFilter adapt(final Filter<File> toAdapt) {

        // Check sanity
        Validate.notNull(toAdapt, "toAdapt");

        // Already a FileFilter?
        if (toAdapt instanceof FileFilter) {
            return (FileFilter) toAdapt;
        }

        // Wrap and return.
        return new FileFilter() {
            @Override
            public boolean accept(final File candidate) {
                return toAdapt.accept(candidate);
            }
        };
    }

    /**
     * Adapts the supplied List of Filter specifications to a List of FileFilters.
     *
     * @param toAdapt The List of Filters to adapts.
     * @return A List holding FileFilter instances. If {@code toAdapt} is {@code null} or empty, an empty list is
     * returned from this method. Thus, this method will never return a {@code null} value.
     */
    public static List<FileFilter> adapt(final List<Filter<File>> toAdapt) {

        final List<FileFilter> toReturn = new ArrayList<FileFilter>();
        if (toAdapt != null) {
            for (Filter<File> current : toAdapt) {
                toReturn.add(adapt(current));
            }
        }

        // All done.
        return toReturn;
    }

    /**
     * Initializes the supplied Filters by assigning the given Log.
     *
     * @param log     The active Maven Log.
     * @param filters The List of Filters to initialize.
     * @param <T>     The Filter type.
     */
    public static <T> void initialize(final Log log, final List<Filter<T>> filters) {

        // Check sanity
        Validate.notNull(log, "log");
        Validate.notNull(filters, "filters");

        for (Filter<T> current : filters) {
            current.initialize(log);
        }
    }

    /**
     * Initializes the supplied Filters by assigning the given Log.
     *
     * @param log     The active Maven Log.
     * @param filters The List of Filters to initialize.
     * @param <T>     The Filter type.
     */
    public static <T> void initialize(final Log log, final Filter<T>... filters) {

        // Check sanity
        Validate.notNull(log, "log");

        if (filters != null) {
            for (Filter<T> current : filters) {
                current.initialize(log);
            }
        }
    }
} public class PatternFileFilter extends AbstractPatternFilter<File> implements FileFilter {

    /**
     * Java RegExp pattern matching one or more letters/digits/punctuation characters.
     * It can be flexibly used to separate normative text in a pattern:
     * <ol>
     *     <li>Pattern matching <strong>ends of strings</strong>. <code>PATTERN_LETTER_DIGIT_PUNCT + "txt"</code>
     *     matches all file paths ending in "txt", such as "some/foobar.txt"</li>
     *     <li>Pattern matching <strong>strings containing patterns</strong>. <code>PATTERN_LETTER_DIGIT_PUNCT +
     *     "foobar" + PATTERN_LETTER_DIGIT_PUNCT</code> matches all file paths containing "foobar" such as
     *     "the/file/in/directory/foobar/blah.java"</li>
     *     <li>Pattern matching <strong>start of strings</strong>. <code>"/some/prefix"
     *     + PATTERN_LETTER_DIGIT_PUNCT</code> matches all file paths starting in "/some/prefix", such as
     *     "some/prefix/another/specification.xsd"</li>
     * </ol>
     */
    public static final String PATTERN_LETTER_DIGIT_PUNCT = "(\\p{javaLetterOrDigit}|\\p{Punct})+";

    /**
     * Converter returning the canonical and absolute path for a File.
     */
    public static final StringConverter<File> FILE_PATH_CONVERTER = new StringConverter<File>() {
        @Override
        public String convert(final File toConvert) {
            return FileSystemUtilities.getCanonicalPath(toConvert.getAbsoluteFile());
        }
    };

    /**
     * Compound constructor creating an PatternFileFilter from the supplied parameters.
     *
     * @param processNullValues             if {@code true}, this PatternFileFilter process null candidate values.
     * @param patternPrefix                 a prefix to be prepended to any patterns submitted to
     *                                      this PatternFileFilter
     * @param patterns                      The non-null list of Patters which should be applied within this
     *                                      PatternFileFilter.
     * @param converter                     The StringConverter which converts Files to Strings for Pattern matching.
     * @param acceptCandidateOnPatternMatch if {@code true}, this PatternFileFilter will matchAtLeastOnce
     *                                      candidate objects that match at least one of the supplied patterns.
     *                                      if {@code false}, this PatternFileFilter will noFilterMatches
     *                                      candidates that match at least one of the supplied patterns.
     */
    public PatternFileFilter(final boolean processNullValues,
            final String patternPrefix,
            final List<String> patterns,
            final StringConverter<File> converter,
            final boolean acceptCandidateOnPatternMatch) {
        super();

        // Assign internal state
        setProcessNullValues(processNullValues);
        setAcceptCandidateOnPatternMatch(acceptCandidateOnPatternMatch);
        setPatternPrefix(patternPrefix);
        setPatterns(patterns);
        setConverter(converter);
    }

    /**
     * Creates a new PatternFileFilter using the supplied patternStrings which are interpreted as file suffixes.
     * (I.e. prepended with {@code PATTERN_LETTER_DIGIT_PUNCT} and compiled to Patterns).
     * The {@code FILE_PATH_CONVERTER} is used to convert Files to strings.
     * The supplied {@code acceptCandidateOnPatternMatch} parameter indicates if this
     * PatternFileFilter accepts or rejects candidates that match any of the supplied patternStrings.
     *
     * @param patternStrings                The list of patternStrings to be used as file path suffixes.
     * @param acceptCandidateOnPatternMatch if {@code true}, this PatternFileFilter will matchAtLeastOnce
     *                                      candidate objects that match at least one of the supplied patterns.
     *                                      if {@code false}, this PatternFileFilter will noFilterMatches
     *                                      candidates that match at least one of the supplied patterns.
     * @see #FILE_PATH_CONVERTER
     * @see #PATTERN_LETTER_DIGIT_PUNCT
     * @see #convert(java.util.List, String)
     */
    public PatternFileFilter(final List<String> patternStrings, final boolean acceptCandidateOnPatternMatch) {
        this(false, PATTERN_LETTER_DIGIT_PUNCT, patternStrings, FILE_PATH_CONVERTER, acceptCandidateOnPatternMatch);
    }

    /**
     * Creates a new PatternFileFilter using the supplied patternStrings which are interpreted as file suffixes.
     * (I.e. prepended with {@code PATTERN_LETTER_DIGIT_PUNCT} and compiled to Patterns).
     * The {@code FILE_PATH_CONVERTER} is used to convert Files to strings.
     * The retrieved PatternFileFilter accepts candidates that match any of the supplied patternStrings.
     *
     * @param patterns The list of patternStrings to be used as file path suffixes.
     */
    public PatternFileFilter(final List<String> patterns) {
        this(false, PATTERN_LETTER_DIGIT_PUNCT, patterns, FILE_PATH_CONVERTER, true);
    }

    /**
     * <p>Creates a new PatternFileFilter with no patternStrings List, implying that calling this constructor must be
     * followed by a call to the {@code #setPatterns} method.</p>
     * <p>The default prefix is {@code PATTERN_LETTER_DIGIT_PUNCT}, the default StringConverter is
     * {@code FILE_PATH_CONVERTER} and this PatternFileFilter does by default accept candidates that match any of
     * the supplied PatternStrings (i.e. an include-mode filter)</p>
     */
    public PatternFileFilter() {
        this(false, PATTERN_LETTER_DIGIT_PUNCT, new ArrayList<String>(), FILE_PATH_CONVERTER, true);
    }

    /**
     * Creates a new List containing an exclude-mode PatternFileFilter using the supplied patternStrings which
     * are interpreted as file suffixes. (I.e. prepended with {@code PATTERN_LETTER_DIGIT_PUNCT} and compiled to
     * Patterns). The {@code FILE_PATH_CONVERTER} is used to convert Files to strings.
     *
     * @param patterns A List of suffix patterns to be used in creating a new ExclusionRegularExpressionFileFilter.
     * @param log      The active Maven Log.
     * @return A List containing a PatternFileFilter using the supplied suffix patterns to match Files.
     * @see PatternFileFilter
     */
    public static List<Filter<File>> createExcludeFilterList(final Log log,
            final String... patterns) {
        return createFilterList(log, false, patterns);
    }

    /**
     * Creates a new List containing an include-mode PatternFileFilter using the supplied patternStrings which
     * are interpreted as file suffixes. (I.e. prepended with {@code PATTERN_LETTER_DIGIT_PUNCT} and compiled to
     * Patterns). The {@code FILE_PATH_CONVERTER} is used to convert Files to strings.
     *
     * @param patterns A List of suffix patterns to be used in creating a new ExclusionRegularExpressionFileFilter.
     * @param log      The active Maven Log.
     * @return A List containing a PatternFileFilter using the supplied suffix patterns to match Files.
     * @see PatternFileFilter
     */
    public static List<Filter<File>> createIncludeFilterList(final Log log,
            final String... patterns) {
        return createFilterList(log, true, patterns);
    }

    //
    // Private helpers
    //

    private static List<Filter<File>> createFilterList(final Log log,
            final boolean includeOperation,
            final String... patterns) {

        // Check sanity
        Validate.notNull(patterns, "patterns");
        Validate.notNull(log, "log");

        // Convert and return.
        final List<Filter<File>> toReturn = new ArrayList<Filter<File>>();
        final List<String> patternStrings = Arrays.asList(patterns);
        toReturn.add(new PatternFileFilter(patternStrings, includeOperation));

        // Initialize the filters.
        Filters.initialize(log, toReturn);
        return toReturn;
    }
}
import org.apache.maven.plugin.MojoExecutionException;
import org.codehaus.mojo.jaxb2.BufferingLog;
import org.codehaus.mojo.jaxb2.schemageneration.XsdGeneratorHelper;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.AbstractSourceCodeAwareNodeProcessingTest;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocExtractor;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.NoAuthorJavaDocRenderer;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SearchableDocumentation;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.XsdAnnotationProcessor;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.XsdEnumerationAnnotationProcessor;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.ChangeNamespacePrefixProcessor;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.SimpleNamespaceResolver;
import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.schemaenhancement.TransformSchema;
import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities;
import org.codehaus.mojo.jaxb2.shared.filters.Filter;
import org.codehaus.mojo.jaxb2.shared.filters.Filters;
import org.codehaus.mojo.jaxb2.shared.filters.pattern.PatternFileFilter;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.ElementNameAndAttributeQualifier;
import org.custommonkey.xmlunit.XMLAssert;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3c.dom.Document;
import se.jguru.shared.algorithms.api.resources.PropertyResources;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerFactory;
import java.io.File;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

        }
        catch ( MojoExecutionException e )
        {
            Assert.fail( "Two schemas with null Prefix should not yield a MojoExecutionException." );
        }
    }

    @Test
    public void validateExceptionThrownOnDuplicateFiles()
    {

        // Assemble
        final TransformSchema transformSchema1 = new TransformSchema( "foo", "foo", "foo.xsd" );
        final TransformSchema transformSchema2 = new TransformSchema( "bar", "bar", "foo.xsd" );

        final List<TransformSchema> transformSchemas = new ArrayList<TransformSchema>();
        transformSchemas.add( transformSchema1 );
        transformSchemas.add( transformSchema2 );

        // Act & Assert
        try
        {
            XsdGeneratorHelper.validateSchemasInPluginConfiguration( transformSchemas );
            Assert.fail( "Two schemas with same Files should yield a MojoExecutionException." );
        }
        catch ( MojoExecutionException e )
        {
            // Validate the error message.
            String expectedMessage = "Misconfiguration detected: Duplicate 'file' property with value [foo.xsd] " + "found in plugin configuration. Correct schema elements index (0) and (1), " + "to ensure that all 'file' values are unique.";
            Assert.assertEquals( expectedMessage, e.getLocalizedMessage() );
        }
    }

    @Test( expected = MojoExecutionException.class )
    public void validateExceptionThrownOnOnlyUriGiven() throws MojoExecutionException
    {
        // Assemble
        final TransformSchema transformSchema1 = new TransformSchema( "foo", null, "" );

        final List<TransformSchema> transformSchemas = new ArrayList<TransformSchema>();
        transformSchemas.add( transformSchema1 );

        // Act & Assert
        XsdGeneratorHelper.validateSchemasInPluginConfiguration( transformSchemas );
        Assert.fail( "A schema definition with no prefix or file should yield a MojoExecutionException." );
    }

    @Test( expected = MojoExecutionException.class )
    public void validateExceptionThrownOnNullUri() throws MojoExecutionException
    {

        // Assemble
        final TransformSchema transformSchema1 = new TransformSchema( null, "foo", "bar" );

        final List<TransformSchema> transformSchemas = new ArrayList<TransformSchema>();
        transformSchemas.add( transformSchema1 );

        // Act & Assert
        XsdGeneratorHelper.validateSchemasInPluginConfiguration( transformSchemas );
        Assert.fail( "A schema definition with null URI should yield a MojoExecutionException." );
    }

    @Test( expected = MojoExecutionException.class )
    public void validateExceptionThrownOnEmptyUri() throws MojoExecutionException
    {

        // Assemble
        final TransformSchema transformSchema1 = new TransformSchema( "", "foo", "bar" );

        final List<TransformSchema> transformSchemas = new ArrayList<TransformSchema>();
        transformSchemas.add( transformSchema1 );

        // Act & Assert
        XsdGeneratorHelper.validateSchemasInPluginConfiguration( transformSchemas );
        Assert.fail( "A schema definition with empty URI should yield a MojoExecutionException." );
    }

    @Test
    public void validateProcessingNodes()
    {

        // Assemble
        final String newPrefix = "changedFoo";
        final String oldPrefix = "foo";
        final String originalXml = getXmlDocumentSample( oldPrefix );
        final String changedXml = getXmlDocumentSample( newPrefix );
        final NodeProcessor changeNamespacePrefixProcessor = new ChangeNamespacePrefixProcessor( oldPrefix, newPrefix );

        // Act
        final Document processedDocument = XsdGeneratorHelper.parseXmlStream( new StringReader( originalXml ) );
        XsdGeneratorHelper.process( processedDocument.getFirstChild(), true, changeNamespacePrefixProcessor );

        // Assert
        final Document expectedDocument = XsdGeneratorHelper.parseXmlStream( new StringReader( changedXml ) );
        final Diff diff = new Diff( expectedDocument, processedDocument, null, new ElementNameAndAttributeQualifier() );
        diff.overrideElementQualifier( new ElementNameAndAttributeQualifier() );

        XMLAssert.assertXMLEqual( processedDocument, expectedDocument );
    }

    @Test
    public void validateProcessingXSDsWithEnumerations() throws Exception
    {

        // Assemble
        final BufferingLog log = new BufferingLog();
        final JavaDocExtractor extractor = new JavaDocExtractor( log );
        extractor.setEncoding( "UTF-8" );

        final String parentPath = "testdata/schemageneration/javadoc/enums/";
        final URL parentPathURL = getClass().getClassLoader().getResource( parentPath );
        Assert.assertNotNull( parentPathURL );

        final File parentDir = new File( parentPathURL.getPath() );
        Assert.assertTrue( parentDir.exists() && parentDir.isDirectory() );

        final List<Filter<File>> excludeFilesMatching = new ArrayList<Filter<File>>();
        excludeFilesMatching.add( new PatternFileFilter( Collections.singletonList( "\\.xsd" ) ) );
        Filters.initialize( log, excludeFilesMatching );


        final List<File> allSourceFiles = FileSystemUtilities.filterFiles( parentDir, null, parentDir.getAbsolutePath(),
