blob: fe9840bfb46a57007296a7fdaaab49f155e9a4b4 [file] [log] [blame]
/*
* 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.mojo;
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>.
* Apart 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</a>.
*/
@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;
}
}