You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by kw...@apache.org on 2022/03/09 17:55:18 UTC

[jackrabbit-filevault-package-maven-plugin] branch master updated: JCRVLT-558 add mojo for generating a CND (#69)

This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jackrabbit-filevault-package-maven-plugin.git


The following commit(s) were added to refs/heads/master by this push:
     new 386895f  JCRVLT-558 add mojo for generating a CND (#69)
386895f is described below

commit 386895f166b9215dbae21d8e9fd0cc131bff6853
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Wed Mar 9 18:55:15 2022 +0100

    JCRVLT-558 add mojo for generating a CND (#69)
---
 pom.xml                                            |   4 +-
 .../AbstractSourceAndMetadataPackageMojo.java      |   5 +-
 .../filevault/maven/packaging/GenerateCndMojo.java | 274 +++++++++++++++++++++
 .../maven/packaging/GenerateCndMojoTest.java       |  88 +++++++
 .../maven/packaging/GenerateMetadataMojoTest.java  |   2 -
 .../maven/packaging/it/CndGenerationIT.java        |  63 +++++
 .../maven/packaging/it/util/ProjectBuilder.java    |  14 ++
 .../filevault/maven/packaging/cq-nodetypes.cnd     |  91 +++++++
 .../apps/test/base/components/.content.xml         |  19 ++
 .../assetselection/clientlib/.content.xml          |  24 ++
 .../test/base/components/container/.content.xml    |  22 ++
 .../jcr_root/apps/test/base/components/test.txt    |  17 ++
 .../apps/test/base/components/.content.xml         |  19 ++
 .../jcr_root/apps/test/base/components/test.txt    |  17 ++
 .../test-projects/cnd-generation/simple/pom.xml    |  52 ++++
 15 files changed, 705 insertions(+), 6 deletions(-)

diff --git a/pom.xml b/pom.xml
index d5eb8e5..def122b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
         <groupId>org.apache.jackrabbit.vault</groupId>
         <artifactId>parent</artifactId>
         <relativePath />
-        <version>3.5.9-SNAPSHOT</version>
+        <version>3.6.0</version>
     </parent>
 
     <!-- ====================================================================== -->
@@ -32,7 +32,7 @@
     <!-- ====================================================================== -->
     <groupId>org.apache.jackrabbit</groupId>
     <artifactId>filevault-package-maven-plugin</artifactId>
-    <version>1.2.3-SNAPSHOT</version>
+    <version>1.3.0-SNAPSHOT</version>
     <packaging>maven-plugin</packaging>
 
     <name>Apache Jackrabbit FileVault - Package Maven Plugin</name>
diff --git a/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/AbstractSourceAndMetadataPackageMojo.java b/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/AbstractSourceAndMetadataPackageMojo.java
index 25bae89..f66a10d 100644
--- a/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/AbstractSourceAndMetadataPackageMojo.java
+++ b/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/AbstractSourceAndMetadataPackageMojo.java
@@ -22,6 +22,7 @@ import java.util.Set;
 import org.apache.maven.plugin.logging.Log;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.shared.utils.io.DirectoryScanner;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Commons ancestor for all mojos dealing with package source files and meta data files
@@ -90,11 +91,11 @@ public abstract class AbstractSourceAndMetadataPackageMojo extends AbstractMetad
     @Parameter(defaultValue = "true")
     protected boolean addDefaultExcludes;
 
-    protected File getJcrSourceDirectory() {
+    protected @Nullable File getJcrSourceDirectory() {
         return getJcrSourceDirectory(jcrRootSourceDirectory, builtContentDirectory, getLog());
     }
     
-    protected static File getJcrSourceDirectory(File[] jcrRootSourceDirectory, File builtContentDirectory, Log log) {
+    protected static @Nullable File getJcrSourceDirectory(File[] jcrRootSourceDirectory, File builtContentDirectory, Log log) {
         final File jcrSourceDirectory;
         if (builtContentDirectory != null) {
             log.warn("The 'builtContentDirectory' is deprecated. Please use the new 'jcrRootSourceDirectory' instead.");
diff --git a/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateCndMojo.java b/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateCndMojo.java
new file mode 100644
index 0000000..0d0f738
--- /dev/null
+++ b/src/main/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateCndMojo.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.filevault.maven.packaging;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeDefinitionProvider;
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.spi.QNodeDefinition;
+import org.apache.jackrabbit.spi.QNodeTypeDefinition;
+import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
+import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
+import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
+import org.apache.jackrabbit.spi.commons.nodetype.compact.CompactNodeTypeDefWriter;
+import org.apache.jackrabbit.vault.fs.io.DocViewParser;
+import org.apache.jackrabbit.vault.fs.io.DocViewParser.XmlParseException;
+import org.apache.jackrabbit.vault.fs.io.DocViewParserHandler;
+import org.apache.jackrabbit.vault.util.Constants;
+import org.apache.jackrabbit.vault.util.DocViewNode2;
+import org.apache.jackrabbit.vault.util.StandaloneManagerProvider;
+import org.apache.jackrabbit.vault.validation.spi.util.classloaderurl.CndUtil;
+import org.apache.jackrabbit.vault.validation.spi.util.classloaderurl.URLFactory;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.jetbrains.annotations.NotNull;
+import org.xml.sax.InputSource;
+
+/**
+ * Generates a <a href="https://jackrabbit.apache.org/jcr/node-type-notation.html">CND file</a> containing all
+ * used node types and namespaces. It uses the <a href="https://s.apache.org/jcr-2.0-spec/3_Repository_Model.html#3.7.11%20Standard%20Application%20Node%20Types">default namespaces and node types</a>
+ * and in addition some provided ones as source node type and namespace registry.
+ * From those only the ones being really leveraged in the package end up in the generated CND.
+ * The generated CND will end up in the <a href="https://jackrabbit.apache.org/filevault/nodetypes.html">package metadata</a> and 
+ * all contained namespaces and node types are automatically registered during installation (in case they are not yet registered).
+ * @since 1.3.0
+ */
+@Mojo(
+        name = "generate-cnd",
+        defaultPhase = LifecyclePhase.GENERATE_SOURCES, 
+        requiresDependencyResolution = ResolutionScope.COMPILE,
+        threadSafe = true
+)
+public class GenerateCndMojo extends AbstractSourceAndMetadataPackageMojo {
+    /**
+     * List of URLs pointing to a <a href="https://jackrabbit.apache.org/jcr/node-type-notation.html">CND</a>
+     * which define the additional namespaces and node types potentially used in this package apart from the 
+     * <a href="https://s.apache.org/jcr-2.0-spec/3_Repository_Model.html#3.7.11%20Standard%20Application%20Node%20Types">default ones defined in JCR 2.0</a>.
+     * If a URI is pointing to a JAR it will leverage all the node types being mentioned in the 
+     * <a href="https://sling.apache.org/documentation/bundles/content-loading-jcr-contentloader.html#declared-node-type-registration">{@code Sling-Nodetypes} manifest header</a>.
+     * part from the <a href="https://docs.oracle.com/javase/8/docs/api/java/net/URL.html#URL-java.lang.String-java.lang.String-int-java.lang.String-"standard protocols</a> the scheme {@code tccl} 
+     * can be used to reference names from the <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#getContextClassLoader--">Thread's context class loader</a>.
+     * In the Maven plugin context this is the <a href="http://maven.apache.org/guides/mini/guide-maven-classloading.html?ref=driverlayer.com/web#3-plugin-classloaders">plugin classloader.
+     */
+    @Parameter(property = "vault.inputCndUrls")
+    List<String> additionalInputCndUrls = new LinkedList<>();
+
+    public GenerateCndMojo() {
+        
+    }
+
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        getLog().info("Retrieving used node types and namespaces...");
+        try {
+            StandaloneManagerProvider managerProvider = new StandaloneManagerProvider();
+            URLFactory.processUrlStreams(CndUtil.resolveJarUrls(additionalInputCndUrls), t -> {
+                try {
+                    managerProvider.registerNodeTypes(t);
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                }
+                catch (ParseException | RepositoryException e) {
+                    throw new IllegalArgumentException(e);
+                }
+            });
+            File cndOutputFile = new File(getGeneratedVaultDir(true), Constants.NODETYPES_CND);
+            File jcrSourceDirectory = getJcrSourceDirectory();
+            if (jcrSourceDirectory == null) {
+                getLog().warn("None of the configured jcrRootSourceDirectory directories exists, skipping generating the CND!");
+            } else {
+                int numWrittenNodeTypes = generateCnd(managerProvider, cndOutputFile.toPath(), getJcrSourceDirectory().toPath());
+                getLog().info("Written " + numWrittenNodeTypes + " node types to CND file " + getProjectRelativeFilePath(cndOutputFile));
+            }
+        } catch (IOException | RepositoryException | ParseException | IllegalStateException e) {
+            throw new MojoExecutionException("Error while writing CND: " + e.toString(), e);
+        }
+    }
+
+    public int generateCnd(StandaloneManagerProvider managerProvider, Path cndOutputFile, Path jcrSourceDirectory) throws IOException, RepositoryException, ParseException {
+        DocViewParser docViewParser = new DocViewParser(managerProvider.getNamespaceResolver());
+        // traverse relevant package files
+        final Set<String> nodeTypes;
+        try {
+            nodeTypes = collectNodeTypes(jcrSourceDirectory, docViewParser);
+        } catch (UncheckedIOException e) {
+            throw e.getCause();
+        }
+        getLog().info("Found " + nodeTypes.size() + " unique node types" );
+        Collection<? extends QNodeTypeDefinition> ntDefinitons = resolveNodeTypes(nodeTypes, managerProvider.getNameResolver(),
+                managerProvider.getNodeTypeDefinitionProvider());
+
+        // make sure parent directories exist
+        Files.createDirectories(cndOutputFile.getParent());
+        // writes the CND into the given file
+        try (Writer writer = Files.newBufferedWriter(cndOutputFile, StandardCharsets.US_ASCII)) {
+            return writeCnd(ntDefinitons, managerProvider.getNodeTypeDefinitionProvider(), managerProvider.getNamespaceResolver(), writer);
+        }
+    }
+
+    private Set<String> collectNodeTypes(Path jcrRootPath, DocViewParser docViewParser) throws IOException {
+        Set<String> nodeTypes = new HashSet<>();
+        // add default ones for simple file aggregates: https://jackrabbit.apache.org/filevault/vaultfs.html#simple-file-aggregates 
+        nodeTypes.add(JcrConstants.NT_FILE);
+        nodeTypes.add(JcrConstants.NT_FOLDER);
+
+        NodeTypeCollectorHandler nodeTypeCollectorHandler = new NodeTypeCollectorHandler(nodeTypes);
+        // extract types from docview files
+        try (Stream<Path> filePaths = Files.find(jcrRootPath, 50, (path, attributes) -> !attributes.isDirectory() && path.getFileName().toString().endsWith(".xml"))) {
+            filePaths.forEach(p -> {
+                nodeTypeCollectorHandler.setFile(p);
+                getNodeTypes(jcrRootPath, p, docViewParser, nodeTypeCollectorHandler);
+            });
+        }
+        return nodeTypes;
+    }
+
+    private void getNodeTypes(Path jcrRootPath, Path docViewFile, DocViewParser docViewParser, NodeTypeCollectorHandler nodeTypeCollectorHandler) {
+        try (InputStream is = Files.newInputStream(docViewFile);
+             BufferedInputStream bufferedIs = new BufferedInputStream(is)) {
+            String documentViewXmlRootNodePath = DocViewParser.getDocumentViewXmlRootNodePath(bufferedIs, jcrRootPath.relativize(docViewFile));
+            if (documentViewXmlRootNodePath == null) {
+                return;
+            }
+            InputSource inputSource = new InputSource(bufferedIs);
+            docViewParser.parse(documentViewXmlRootNodePath, inputSource, nodeTypeCollectorHandler);
+        } catch (XmlParseException|IOException e) {
+            getLog().warn("Could not parse " + docViewFile + ". Ignore for node type definition generation!", e);
+        }
+    }
+
+    final class NodeTypeCollectorHandler implements DocViewParserHandler {
+        private final Set<String> nodeTypes;
+        private Path file;
+        public NodeTypeCollectorHandler(Set<String> nodeTypes) {
+            this.nodeTypes = nodeTypes;
+        }
+
+        public void setFile(Path file) {
+            this.file = file;
+        }
+
+        @Override
+        public void startDocViewNode(@NotNull String nodePath, @NotNull DocViewNode2 docViewNode, @NotNull Optional<DocViewNode2> parentDocViewNode, int line, int column)
+                throws IOException, RepositoryException {
+            Optional<String> primaryType = docViewNode.getPrimaryType();
+            if (primaryType.isPresent()) {
+                if (nodeTypes.add(primaryType.get())) {
+                    getLog().debug("Found primary type " + primaryType.get() + " in node '" + nodePath + "' contained in file " + getProjectRelativeFilePath(file.toFile()));
+                }
+            }
+            if (nodeTypes.addAll(docViewNode.getMixinTypes())) {
+                getLog().debug("Found mixin types " + docViewNode.getMixinTypes() + " in node '" + nodePath + "' contained in file " + getProjectRelativeFilePath(file.toFile()));
+            }
+        }
+
+        @Override
+        public void endDocViewNode(@NotNull String nodePath, @NotNull DocViewNode2 docViewNode, @NotNull Optional<DocViewNode2> parentDocViewNode, int line, int column)
+                throws IOException, RepositoryException {
+            // do nothing
+        }
+    }
+
+    // resolve from cnd file
+    private Collection<QNodeTypeDefinition> resolveNodeTypes(Set<String> nodeTypeNames, NameResolver nameResolver, NodeTypeDefinitionProvider ntDefinitionProvider) throws IOException, RepositoryException, ParseException {
+        return resolveNodeTypesFromNames(nodeTypeNames.stream()
+                .map(name -> {
+                    try {
+                        return nameResolver.getQName(name);
+                    } catch (IllegalNameException|NamespaceException e) {
+                        throw new IllegalStateException("Cannot get expanded name for type " + name, e);
+                    }
+                })
+                .collect(Collectors.toSet()), 
+                ntDefinitionProvider);
+    }
+
+    static Collection<QNodeTypeDefinition> resolveNodeTypesFromNames(Set<Name> nodeTypeNames, NodeTypeDefinitionProvider ntDefinitionProvider) throws IOException, RepositoryException, ParseException {
+        return nodeTypeNames.stream().map(name -> {
+            try {
+                return ntDefinitionProvider.getNodeTypeDefinition(name);
+            } catch (RepositoryException e) {
+                throw new IllegalStateException("RepositoryException ", e);
+            }
+        }).collect(Collectors.toList());
+    }
+
+    private int writeCnd(Collection<? extends QNodeTypeDefinition> nodeTypeDefinitions, NodeTypeDefinitionProvider ntDefinitionProvider, NamespaceResolver nsResolver, Writer writer) throws IOException, RepositoryException {
+        CompactNodeTypeDefWriter cndWriter = new CompactNodeTypeDefWriter(writer, nsResolver, true);
+        Set<Name> written = new HashSet<>();
+        int numWrittenNodeTypes = 0;
+        for (QNodeTypeDefinition nodeTypeDefinition : nodeTypeDefinitions) {
+            numWrittenNodeTypes += writeNodeType(nodeTypeDefinition, cndWriter, written, ntDefinitionProvider);
+        }
+        cndWriter.close();
+        return numWrittenNodeTypes;
+    }
+
+    private int writeNodeType(Name nodeType, CompactNodeTypeDefWriter cndWriter, Set<Name> written, NodeTypeDefinitionProvider ntDefinitionProvider) throws IOException, RepositoryException {
+        if (nodeType == null || written.contains(nodeType)) {
+            return 0;
+        }
+        QNodeTypeDefinition ntDefinition = ntDefinitionProvider.getNodeTypeDefinition(nodeType);
+        return writeNodeType(ntDefinition, cndWriter, written, ntDefinitionProvider);
+    }
+
+    private int writeNodeType(QNodeTypeDefinition ntDefinition, CompactNodeTypeDefWriter cndWriter, Set<Name> written, NodeTypeDefinitionProvider ntDefinitionProvider)
+            throws IOException, RepositoryException {
+        int numWrittenNodeTypes = 1;
+        cndWriter.write(ntDefinition);
+        written.add(ntDefinition.getName());
+        // also write all referenced node types
+        for (Name superType: ntDefinition.getSupertypes()) {
+            numWrittenNodeTypes += writeNodeType(superType, cndWriter, written, ntDefinitionProvider);
+        }
+        for (QNodeDefinition cntDefinition: ntDefinition.getChildNodeDefs()) {
+            writeNodeType(cntDefinition.getDefaultPrimaryType(), cndWriter, written, ntDefinitionProvider);
+            if (cntDefinition.getRequiredPrimaryTypes() != null) {
+                for (Name name: cntDefinition.getRequiredPrimaryTypes()) {
+                    numWrittenNodeTypes += writeNodeType(name, cndWriter, written, ntDefinitionProvider);
+                }
+            }
+        }
+        return numWrittenNodeTypes;
+    }
+}
diff --git a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateCndMojoTest.java b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateCndMojoTest.java
new file mode 100644
index 0000000..0fb7628
--- /dev/null
+++ b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateCndMojoTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.filevault.maven.packaging;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Objects;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.vault.util.StandaloneManagerProvider;
+import org.apache.maven.plugin.logging.SystemStreamLog;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class GenerateCndMojoTest {
+
+    @Test
+    void testGenerateCndMojo(@TempDir Path tmpFolder) throws IOException, RepositoryException, ParseException {
+        GenerateCndMojo mojo = new GenerateCndMojo();
+        mojo.setLog(new SystemStreamLog());
+        mojo.project = new MavenProject();
+        mojo.project.setFile(new File(getFile("/test-packages/generate-cnd", tmpFolder).toFile(), "pom.xml"));
+        StandaloneManagerProvider managerProvider = new StandaloneManagerProvider();
+        Path cndFile = tmpFolder.resolve("nodetypes.cnd");
+
+        Exception e = assertThrows(IllegalStateException.class, () -> mojo.generateCnd(managerProvider, cndFile, getFile("/test-packages/generate-cnd/jcr_root", tmpFolder)));
+        assertEquals("Cannot get expanded name for type cq:ClientLibraryFolder", e.getMessage());
+
+        // now register additional CND file and try again
+        try (InputStream input = getStream("cq-nodetypes.cnd");
+             Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
+             managerProvider.registerNodeTypes(reader);
+        }
+        assertEquals(11, mojo.generateCnd(managerProvider, cndFile, getFile("/test-packages/generate-cnd/jcr_root", tmpFolder)));
+    }
+
+    public Path getFile(String name, Path tmpFolder) throws IOException {
+        URI uri;
+        try {
+            uri = Objects.requireNonNull(getClass().getResource(name),  "Could not find class resource with name '" + name + "'").toURI();
+        } catch (URISyntaxException e) {
+            throw new IOException("Could not convert class resource URL to URI", e);
+        }
+        if (uri.isOpaque()) { // non hierarchical URIs (for resources in a JAR)  can not use classical file operations
+            Path tmpFile = Files.createTempFile(tmpFolder, null, null);
+            try (InputStream in = getStream(name)) {
+                Files.copy(in, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+            }
+            return tmpFile;
+        } else {
+            return Paths.get(uri);
+        }
+    }
+
+    public InputStream getStream(String name) {
+        return Objects.requireNonNull(getClass().getResourceAsStream(name), "Could not find class resource with name '" + name + "'");
+    }
+}
diff --git a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateMetadataMojoTest.java b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateMetadataMojoTest.java
index a836397..c77807c 100644
--- a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateMetadataMojoTest.java
+++ b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/GenerateMetadataMojoTest.java
@@ -171,6 +171,4 @@ public class GenerateMetadataMojoTest {
     private static String unescapeContinuations(String value) {
         return value.replaceAll("\r ", "");
     }
-    
-    
 }
diff --git a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/CndGenerationIT.java b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/CndGenerationIT.java
new file mode 100644
index 0000000..6a3eafa
--- /dev/null
+++ b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/CndGenerationIT.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.filevault.maven.packaging.it;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.jackrabbit.filevault.maven.packaging.it.util.ProjectBuilder;
+import org.apache.jackrabbit.filevault.maven.packaging.it.util.ProjectBuilderExtension;
+import org.apache.maven.it.VerificationException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(ProjectBuilderExtension.class)
+class CndGenerationIT {
+
+    @Test
+    void testCndGeneration(ProjectBuilder projectBuilder) throws IOException, VerificationException {
+        String expectedCnd = "<'nt'='http://www.jcp.org/jcr/nt/1.0'>\n"
+                + "<'jcr'='http://www.jcp.org/jcr/1.0'>\n"
+                + "<'mix'='http://www.jcp.org/jcr/mix/1.0'>\n"
+                + "\n"
+                + "[nt:file] > nt:hierarchyNode\n"
+                + "  primaryitem jcr:content\n"
+                + "  + jcr:content (nt:base) mandatory\n"
+                + "\n"
+                + "[nt:hierarchyNode] > mix:created\n"
+                + "  abstract\n"
+                + "\n"
+                + "[mix:created]\n"
+                + "  mixin\n"
+                + "  - jcr:createdBy (string) autocreated protected\n"
+                + "  - jcr:created (date) autocreated protected\n"
+                + "\n"
+                + "[nt:base]\n"
+                + "  abstract\n"
+                + "  - jcr:mixinTypes (name) protected multiple compute\n"
+                + "  - jcr:primaryType (name) mandatory autocreated protected compute\n"
+                + "\n"
+                + "[nt:folder] > nt:hierarchyNode\n"
+                + "  + * (nt:hierarchyNode) version\n"
+                + "\n";
+        
+        projectBuilder
+                .setTestProjectDir("cnd-generation/simple")
+                .build()
+                .verifyExpectedFileContent("META-INF/vault/nodetypes.cnd", StandardCharsets.US_ASCII, expectedCnd);
+    }
+}
diff --git a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/util/ProjectBuilder.java b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/util/ProjectBuilder.java
index a375a5a..5c07465 100644
--- a/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/util/ProjectBuilder.java
+++ b/src/test/java/org/apache/jackrabbit/filevault/maven/packaging/it/util/ProjectBuilder.java
@@ -26,8 +26,10 @@ import static org.junit.jupiter.api.Assertions.fail;
 import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.ArrayList;
@@ -385,6 +387,18 @@ public class ProjectBuilder implements AutoCloseable {
         return this;
     }
 
+    public ProjectBuilder verifyExpectedFileContent(String name, Charset cs, String expectedContent) throws IOException {
+        openPackage();
+        JarEntry entry = contentPackageZip.getJarEntry(name);
+        if (entry == null) {
+            fail("Could not find entry with name " + name + " in package " + testPackageFile);
+        }
+        try (InputStream actualInput = contentPackageZip.getInputStream(entry)) {
+            assertEquals(expectedContent, IOUtils.toString(actualInput, cs));
+        }
+        return this;
+    }
+
     private final static class JarEntryMatcher extends TypeSafeMatcher<JarEntry> {
 
         private final String name;
diff --git a/src/test/resources/org/apache/jackrabbit/filevault/maven/packaging/cq-nodetypes.cnd b/src/test/resources/org/apache/jackrabbit/filevault/maven/packaging/cq-nodetypes.cnd
new file mode 100644
index 0000000..7d2a8ef
--- /dev/null
+++ b/src/test/resources/org/apache/jackrabbit/filevault/maven/packaging/cq-nodetypes.cnd
@@ -0,0 +1,91 @@
+/*
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+<'sling'='http://sling.apache.org/jcr/sling/1.0'>
+<'cq'='http://www.day.com/jcr/cq/1.0'>
+
+[sling:Folder] > nt:folder
+  - * (undefined)
+  - * (undefined) multiple
+  + * (nt:base) = sling:Folder version
+
+[cq:ClientLibraryFolder] > sling:Folder
+  - channels (string) multiple
+  - categories (string) multiple
+  - dependencies (string) multiple
+  - embed (string) multiple
+
+[cq:Component] > mix:title, nt:folder, sling:ResourceSuperType
+  - * (undefined)
+  - allowedChildren (string) multiple
+  - dialogPath (string)
+  - * (undefined) multiple
+  - cq:isContainer (boolean)
+  - cq:noDecoration (boolean)
+  - cq:cellName (string)
+  - allowedParents (string) multiple
+  - componentGroup (string)
+  + cq:childEditConfig (cq:EditConfig) = cq:EditConfig
+  + design_dialog (nt:base) = nt:unstructured
+  + * (nt:base) = nt:base multiple version
+  + cq:infoProviders (nt:base) = nt:unstructured
+  + virtual (nt:base) = sling:Folder
+  + dialog (nt:base) = nt:unstructured
+  + cq:htmlTag (nt:base) = nt:unstructured
+  + cq:editConfig (cq:EditConfig) = cq:EditConfig
+  + icon.png (nt:file)
+  + thumbnail.png (nt:file)
+
+[sling:ResourceSuperType]
+  mixin
+  - sling:resourceSuperType (string)
+
+[mix:title]
+  mixin
+  - jcr:description (string)
+  - jcr:title (string)
+
+[cq:EditConfig] > nt:hierarchyNode, nt:unstructured
+  orderable
+  - cq:dialogMode (string) < 'auto', 'floating', 'inline'
+  - cq:layout (string) < 'editbar', 'rollover', 'auto'
+  - cq:actions (string) multiple
+  - cq:emptyText (string)
+  - cq:inherit (boolean)
+  + cq:formParameters (nt:base) = nt:unstructured
+  + cq:actionConfigs (nt:base) = nt:unstructured
+  + cq:dropTargets (nt:base) = nt:unstructured
+  + cq:inplaceEditing (cq:InplaceEditingConfig) = cq:InplaceEditingConfig
+  + cq:listeners (nt:base) = cq:EditListenersConfig
+
+[cq:InplaceEditingConfig] > nt:unstructured
+  orderable
+  - editorType (string)
+  - configPath (string)
+  - active (boolean)
+  + config (nt:unstructured) = nt:unstructured
+
+[cq:EditListenersConfig]
+  - afteredit (string)
+  - aftermove (string)
+  - afterinsert (string)
+  - * (undefined) multiple
+  - aftercreate (string)
+  - afterremove (string)
+  - * (undefined)
+  - afterdelete (string)
+  + * (nt:base) = nt:base multiple version
\ No newline at end of file
diff --git a/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/.content.xml b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/.content.xml
new file mode 100644
index 0000000..ce03add
--- /dev/null
+++ b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/.content.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
+    jcr:primaryType="sling:Folder"/>
diff --git a/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/assetselection/clientlib/.content.xml b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/assetselection/clientlib/.content.xml
new file mode 100644
index 0000000..2f6e264
--- /dev/null
+++ b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/assetselection/clientlib/.content.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
+    jcr:primaryType="cq:ClientLibraryFolder"
+    allowProxy="{Boolean}true"
+    categories="[test]"
+    dependencies="[cq.shared]"
+    embed="[cq.authoring.messaging]"
+    jsProcessor="[default:gcc;languageIn=STABLE;compilationLevel=whitespace,min:gcc;languageIn=STABLE;compilationLevel=whitespace]"/>
diff --git a/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/container/.content.xml b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/container/.content.xml
new file mode 100644
index 0000000..a2537ee
--- /dev/null
+++ b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/container/.content.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
+    cq:isContainer="{Boolean}true"
+    jcr:primaryType="cq:Component"
+    jcr:title="Container"
+    sling:resourceSuperType="core/wcm/components/container/v1/container"/>
diff --git a/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/test.txt b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/test.txt
new file mode 100644
index 0000000..6a57763
--- /dev/null
+++ b/src/test/resources/test-packages/generate-cnd/jcr_root/apps/test/base/components/test.txt
@@ -0,0 +1,17 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+Some empty file
\ No newline at end of file
diff --git a/src/test/resources/test-projects/cnd-generation/simple/jcr_root/apps/test/base/components/.content.xml b/src/test/resources/test-projects/cnd-generation/simple/jcr_root/apps/test/base/components/.content.xml
new file mode 100644
index 0000000..e10c257
--- /dev/null
+++ b/src/test/resources/test-projects/cnd-generation/simple/jcr_root/apps/test/base/components/.content.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
+    jcr:primaryType="nt:folder"/>
diff --git a/src/test/resources/test-projects/cnd-generation/simple/jcr_root/apps/test/base/components/test.txt b/src/test/resources/test-projects/cnd-generation/simple/jcr_root/apps/test/base/components/test.txt
new file mode 100644
index 0000000..6a57763
--- /dev/null
+++ b/src/test/resources/test-projects/cnd-generation/simple/jcr_root/apps/test/base/components/test.txt
@@ -0,0 +1,17 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+Some empty file
\ No newline at end of file
diff --git a/src/test/resources/test-projects/cnd-generation/simple/pom.xml b/src/test/resources/test-projects/cnd-generation/simple/pom.xml
new file mode 100644
index 0000000..5e41e58
--- /dev/null
+++ b/src/test/resources/test-projects/cnd-generation/simple/pom.xml
@@ -0,0 +1,52 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.jackrabbit.filevault</groupId>
+    <artifactId>package-plugin-test-pkg</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <packaging>content-package</packaging>
+    <name>Example Default Project</name>
+    <description>This is an Example Project to verify that CND generation is correct.</description>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.jackrabbit</groupId>
+                <artifactId>filevault-package-maven-plugin</artifactId>
+                <version>${plugin.version}</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <group>my/test/group</group>
+                    <filters>
+                        <filter>
+                            <root>/apps/test</root>
+                        </filter>
+                    </filters>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>generate-cnd</id>
+                        <goals>
+                            <goal>generate-cnd</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+   
+</project>