/*
|
* The contents of this file are subject to the terms of the Common Development and
|
* Distribution License (the License). You may not use this file except in compliance with the
|
* License.
|
*
|
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
|
* specific language governing permission and limitations under the License.
|
*
|
* When distributing Covered Software, include this CDDL Header Notice in each file and include
|
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
|
* Header, with the fields enclosed by brackets [] replaced by your own identifying
|
* information: "Portions Copyright [year] [name of copyright owner]".
|
*
|
* Copyright 2013-2016 ForgeRock AS.
|
*/
|
package org.forgerock.opendj.maven;
|
|
import static org.apache.maven.plugins.annotations.LifecyclePhase.*;
|
import static org.apache.maven.plugins.annotations.ResolutionScope.*;
|
|
import java.io.File;
|
import java.io.FileFilter;
|
import java.io.FileOutputStream;
|
import java.io.IOException;
|
import java.net.JarURLConnection;
|
import java.net.URL;
|
import java.util.Enumeration;
|
import java.util.LinkedHashMap;
|
import java.util.LinkedList;
|
import java.util.Map;
|
import java.util.Queue;
|
import java.util.concurrent.Callable;
|
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.Executors;
|
import java.util.concurrent.Future;
|
import java.util.jar.JarEntry;
|
import java.util.jar.JarFile;
|
|
import javax.xml.transform.Source;
|
import javax.xml.transform.Templates;
|
import javax.xml.transform.Transformer;
|
import javax.xml.transform.TransformerConfigurationException;
|
import javax.xml.transform.TransformerException;
|
import javax.xml.transform.TransformerFactory;
|
import javax.xml.transform.URIResolver;
|
import javax.xml.transform.stream.StreamResult;
|
import javax.xml.transform.stream.StreamSource;
|
|
import org.apache.maven.model.Resource;
|
import org.apache.maven.plugin.AbstractMojo;
|
import org.apache.maven.plugin.MojoExecutionException;
|
import org.apache.maven.plugins.annotations.Mojo;
|
import org.apache.maven.plugins.annotations.Parameter;
|
import org.apache.maven.project.MavenProject;
|
|
/**
|
* Generate configuration classes from XML definition files for OpenDJ server.
|
* <p>
|
* There is a single goal that generate java sources, manifest files, I18N
|
* messages and cli/ldap profiles. Resources will be looked for in the following
|
* places depending on whether the plugin is executing for the core config or an
|
* extension:
|
* <table border="1">
|
* <tr>
|
* <th></th>
|
* <th>Location</th>
|
* </tr>
|
* <tr>
|
* <th align="left">XSLT stylesheets</th>
|
* <td>Internal: /config/stylesheets</td>
|
* </tr>
|
* <tr>
|
* <th align="left">XML core definitions</th>
|
* <td>Internal: /config/xml</td>
|
* </tr>
|
* <tr>
|
* <th align="left">XML extension definitions</th>
|
* <td>${basedir}/src/main/java</td>
|
* </tr>
|
* <tr>
|
* <th align="left">Generated Java APIs</th>
|
* <td>${project.build.directory}/generated-sources/config</td>
|
* </tr>
|
* <tr>
|
* <th align="left">Generated I18N messages</th>
|
* <td>${project.build.outputDirectory}/config/messages</td>
|
* </tr>
|
* <tr>
|
* <th align="left">Generated profiles</th>
|
* <td>${project.build.outputDirectory}/config/profiles/${profile}</td>
|
* </tr>
|
* <tr>
|
* <th align="left">Generated manifest</th>
|
* <td>${project.build.outputDirectory}/META-INF/services/org.forgerock.opendj.
|
* config.AbstractManagedObjectDefinition</td>
|
* </tr>
|
* </table>
|
*/
|
@Mojo(name = "generate-config", defaultPhase = GENERATE_SOURCES, requiresDependencyResolution = COMPILE_PLUS_RUNTIME)
|
public final class GenerateConfigMojo extends AbstractMojo {
|
|
private static final String CONFIGURATION_FILE_SUFFIX = "Configuration.xml";
|
|
private interface StreamSourceFactory {
|
StreamSource newStreamSource() throws IOException;
|
}
|
|
/**
|
* The Maven Project.
|
*/
|
@Parameter(required = true, readonly = true, property = "project")
|
private MavenProject project;
|
|
/**
|
* Package name for which artifacts are generated.
|
* <p>
|
* This relative path is used to locate xml definition files and to locate
|
* generated artifacts.
|
*/
|
@Parameter(required = true)
|
private String packageName;
|
|
/**
|
* {@code true} if this plugin should be used to generate classes
|
* for extended configuration (e.g OpenDJ plugins).
|
* <p>
|
* If not specified, OpenDJ configuration classes will be generated.
|
*/
|
@Parameter(required = true, defaultValue = "false")
|
private Boolean isExtension;
|
|
private final Map<String, StreamSourceFactory> componentDescriptors = new LinkedHashMap<>();
|
private TransformerFactory stylesheetFactory;
|
private Templates stylesheetMetaJava;
|
private Templates stylesheetServerJava;
|
private Templates stylesheetClientJava;
|
private Templates stylesheetMetaPackageInfo;
|
private Templates stylesheetServerPackageInfo;
|
private Templates stylesheetClientPackageInfo;
|
private Templates stylesheetProfileLDAP;
|
private Templates stylesheetProfileCLI;
|
private Templates stylesheetMessages;
|
private Templates stylesheetManifest;
|
private final Queue<Future<?>> tasks = new LinkedList<>();
|
|
private final URIResolver resolver = new URIResolver() {
|
|
@Override
|
public synchronized Source resolve(final String href, final String base)
|
throws TransformerException {
|
if (href.endsWith(".xsl")) {
|
final String stylesheet;
|
if (href.startsWith("../")) {
|
stylesheet = "/config/stylesheets/" + href.substring(3);
|
} else {
|
stylesheet = "/config/stylesheets/" + href;
|
}
|
getLog().debug("#### Resolved stylesheet " + href + " to " + stylesheet);
|
return new StreamSource(getClass().getResourceAsStream(stylesheet));
|
} else if (href.endsWith(".xml")) {
|
if (href.startsWith("org/forgerock/opendj/server/config/")) {
|
final String coreXML = "/config/xml/" + href;
|
getLog().debug("#### Resolved core XML definition " + href + " to " + coreXML);
|
return new StreamSource(getClass().getResourceAsStream(coreXML));
|
} else {
|
final String extXML = getXMLDirectory() + "/" + href;
|
getLog().debug(
|
"#### Resolved extension XML definition " + href + " to " + extXML);
|
return new StreamSource(new File(extXML));
|
}
|
} else {
|
throw new TransformerException("Unable to resolve URI " + href);
|
}
|
}
|
};
|
|
@Override
|
public void execute() throws MojoExecutionException {
|
if (getPackagePath() == null) {
|
throw new MojoExecutionException("<packagePath> must be set.");
|
} else if (!isXMLPackageDirectoryValid()) {
|
throw new MojoExecutionException("The XML definition directory \""
|
+ getXMLPackageDirectory() + "\" does not exist.");
|
} else if (getClass().getResource(getStylesheetDirectory()) == null) {
|
throw new MojoExecutionException("The XSLT stylesheet directory \""
|
+ getStylesheetDirectory() + "\" does not exist.");
|
}
|
|
// Validate and transform.
|
try {
|
initializeStylesheets();
|
getLog().info("Loading XML descriptors...");
|
loadXMLDescriptors();
|
getLog().info("Found " + componentDescriptors.size() + " XML descriptors");
|
executeValidateXMLDefinitions();
|
executeTransformXMLDefinitions();
|
getLog().info("Adding source directory \"" + getGeneratedSourcesDirectory() + "\" to build path...");
|
project.addCompileSourceRoot(getGeneratedSourcesDirectory());
|
project.addResource(getGeneratedMavenResources());
|
} catch (final Exception e) {
|
throw new MojoExecutionException("XSLT configuration transformation failed", e);
|
}
|
}
|
|
private Resource getGeneratedMavenResources() {
|
final String[] generatedResourcesRelativePath =
|
new String[] { "/META-INF/services/**", "/config/**/*.properties" };
|
final Resource resources = new Resource();
|
resources.setDirectory(getGeneratedResourcesDirectory());
|
for (final String generatedResourceRelativePath : generatedResourcesRelativePath) {
|
resources.addInclude(generatedResourceRelativePath);
|
getLog().info("Adding resource \"" + getGeneratedResourcesDirectory() + generatedResourceRelativePath
|
+ " to resource path...");
|
}
|
|
return resources;
|
}
|
|
private void createTransformTask(final StreamSourceFactory inputFactory, final StreamResult output,
|
final Templates stylesheet, final ExecutorService executor, final String... parameters)
|
throws Exception {
|
final Future<Void> future = executor.submit(new Callable<Void>() {
|
@Override
|
public Void call() throws Exception {
|
final Transformer transformer = stylesheet.newTransformer();
|
transformer.setURIResolver(resolver);
|
for (int i = 0; i < parameters.length; i += 2) {
|
transformer.setParameter(parameters[i], parameters[i + 1]);
|
}
|
transformer.transform(inputFactory.newStreamSource(), output);
|
return null;
|
}
|
});
|
tasks.add(future);
|
}
|
|
private void createTransformTask(final StreamSourceFactory inputFactory,
|
final String outputFileName, final Templates stylesheet,
|
final ExecutorService executor, final String... parameters) throws Exception {
|
final File outputFile = new File(outputFileName);
|
outputFile.getParentFile().mkdirs();
|
final StreamResult output = new StreamResult(outputFile);
|
createTransformTask(inputFactory, output, stylesheet, executor, parameters);
|
}
|
|
private void executeTransformXMLDefinitions() throws Exception {
|
getLog().info("Transforming XML definitions...");
|
|
/*
|
* Restrict the size of the thread pool in order to throttle
|
* creation of transformers and ZIP input streams and prevent potential
|
* OOME.
|
*/
|
final ExecutorService parallelExecutor = Executors.newFixedThreadPool(16);
|
|
/*
|
* The manifest is a single file containing the concatenated output of
|
* many transformations. Therefore we must ensure that output is
|
* serialized by using a single threaded executor.
|
*/
|
final ExecutorService sequentialExecutor = Executors.newSingleThreadExecutor();
|
final File manifestFile = new File(getGeneratedManifestFile());
|
manifestFile.getParentFile().mkdirs();
|
final FileOutputStream manifestFileOutputStream = new FileOutputStream(manifestFile);
|
final StreamResult manifest = new StreamResult(manifestFileOutputStream);
|
try {
|
/*
|
* Generate Java classes and resources for each XML definition.
|
*/
|
final String javaDir = getGeneratedSourcesDirectory() + "/" + getPackagePath() + "/";
|
final String metaDir = javaDir + "meta/";
|
final String serverDir = javaDir + "server/";
|
final String clientDir = javaDir + "client/";
|
final String ldapProfileDir =
|
getGeneratedProfilesDirectory("ldap") + "/" + getPackagePath() + "/meta/";
|
final String cliProfileDir =
|
getGeneratedProfilesDirectory("cli") + "/" + getPackagePath() + "/meta/";
|
final String i18nDir =
|
getGeneratedMessagesDirectory() + "/" + getPackagePath() + "/meta/";
|
|
for (final Map.Entry<String, StreamSourceFactory> entry : componentDescriptors
|
.entrySet()) {
|
final String meta = metaDir + entry.getKey() + "CfgDefn.java";
|
createTransformTask(entry.getValue(), meta, stylesheetMetaJava, parallelExecutor);
|
|
final String server = serverDir + entry.getKey() + "Cfg.java";
|
createTransformTask(entry.getValue(), server, stylesheetServerJava,
|
parallelExecutor);
|
|
final String client = clientDir + entry.getKey() + "CfgClient.java";
|
createTransformTask(entry.getValue(), client, stylesheetClientJava,
|
parallelExecutor);
|
|
final String ldap = ldapProfileDir + entry.getKey() + "CfgDefn.properties";
|
createTransformTask(entry.getValue(), ldap, stylesheetProfileLDAP, parallelExecutor);
|
|
final String cli = cliProfileDir + entry.getKey() + "CfgDefn.properties";
|
createTransformTask(entry.getValue(), cli, stylesheetProfileCLI, parallelExecutor);
|
|
final String i18n = i18nDir + entry.getKey() + "CfgDefn.properties";
|
createTransformTask(entry.getValue(), i18n, stylesheetMessages, parallelExecutor);
|
|
createTransformTask(entry.getValue(), manifest, stylesheetManifest,
|
sequentialExecutor);
|
}
|
|
// Generate package-info.java files.
|
final Map<String, Templates> profileMap = new LinkedHashMap<>();
|
profileMap.put("meta", stylesheetMetaPackageInfo);
|
profileMap.put("server", stylesheetServerPackageInfo);
|
profileMap.put("client", stylesheetClientPackageInfo);
|
for (final Map.Entry<String, Templates> entry : profileMap.entrySet()) {
|
final StreamSourceFactory sourceFactory = new StreamSourceFactory() {
|
@Override
|
public StreamSource newStreamSource() throws IOException {
|
if (isExtension) {
|
return new StreamSource(new File(getXMLPackageDirectory()
|
+ "/Package.xml"));
|
} else {
|
return new StreamSource(getClass().getResourceAsStream(
|
"/" + getXMLPackageDirectory() + "/Package.xml"));
|
}
|
}
|
};
|
final String profile = javaDir + "/" + entry.getKey() + "/package-info.java";
|
createTransformTask(sourceFactory, profile, entry.getValue(), parallelExecutor,
|
"type", entry.getKey());
|
}
|
|
/*
|
* Wait for all transformations to complete and cleanup. Remove the
|
* completed tasks from the list as we go in order to free up
|
* memory.
|
*/
|
for (Future<?> task = tasks.poll(); task != null; task = tasks.poll()) {
|
task.get();
|
}
|
} finally {
|
parallelExecutor.shutdown();
|
sequentialExecutor.shutdown();
|
manifestFileOutputStream.close();
|
}
|
}
|
|
private void executeValidateXMLDefinitions() {
|
// TODO:
|
getLog().info("Validating XML definitions...");
|
}
|
|
private String getBaseDir() {
|
return project.getBasedir().toString();
|
}
|
|
private String getGeneratedResourcesDirectory() {
|
return project.getBuild().getDirectory() + "/generated-resources";
|
}
|
|
private String getGeneratedManifestFile() {
|
return getGeneratedResourcesDirectory()
|
+ "/META-INF/services/org.forgerock.opendj.config.AbstractManagedObjectDefinition";
|
}
|
|
private String getGeneratedMessagesDirectory() {
|
return getGeneratedResourcesDirectory() + "/config/messages";
|
}
|
|
private String getGeneratedProfilesDirectory(final String profileName) {
|
return getGeneratedResourcesDirectory() + "/config/profiles/" + profileName;
|
}
|
|
private String getGeneratedSourcesDirectory() {
|
return project.getBuild().getDirectory() + "/generated-sources/config";
|
}
|
|
private String getPackagePath() {
|
return packageName.replace('.', '/');
|
}
|
|
private String getStylesheetDirectory() {
|
return "/config/stylesheets";
|
}
|
|
private String getXMLDirectory() {
|
if (isExtension) {
|
return getBaseDir() + "/src/main/java";
|
} else {
|
return "config/xml";
|
}
|
}
|
|
private String getXMLPackageDirectory() {
|
return getXMLDirectory() + "/" + getPackagePath();
|
}
|
|
private void initializeStylesheets() throws TransformerConfigurationException {
|
getLog().info("Loading XSLT stylesheets...");
|
stylesheetFactory = TransformerFactory.newInstance();
|
try {
|
stylesheetFactory.setAttribute("jdk.xml.xpathTotalOpLimit", "0");
|
}catch (Throwable e) {}
|
try {
|
stylesheetFactory.setAttribute("jdk.xml.xpathExprGrpLimit", "0");
|
}catch (Throwable e) {}
|
try {
|
stylesheetFactory.setAttribute("jdk.xml.xpathExprOpLimit", "0");
|
}catch (Throwable e) {}
|
stylesheetFactory.setURIResolver(resolver);
|
stylesheetMetaJava = loadStylesheet("metaMO.xsl");
|
stylesheetMetaPackageInfo = loadStylesheet("package-info.xsl");
|
stylesheetServerJava = loadStylesheet("serverMO.xsl");
|
stylesheetServerPackageInfo = loadStylesheet("package-info.xsl");
|
stylesheetClientJava = loadStylesheet("clientMO.xsl");
|
stylesheetClientPackageInfo = loadStylesheet("package-info.xsl");
|
stylesheetProfileLDAP = loadStylesheet("ldapMOProfile.xsl");
|
stylesheetProfileCLI = loadStylesheet("cliMOProfile.xsl");
|
stylesheetMessages = loadStylesheet("messagesMO.xsl");
|
stylesheetManifest = loadStylesheet("manifestMO.xsl");
|
}
|
|
private boolean isXMLPackageDirectoryValid() {
|
// Not an extension, so always valid.
|
return !isExtension
|
|| new File(getXMLPackageDirectory()).isDirectory();
|
}
|
|
private Templates loadStylesheet(final String stylesheet)
|
throws TransformerConfigurationException {
|
final Source xslt =
|
new StreamSource(getClass().getResourceAsStream(
|
getStylesheetDirectory() + "/" + stylesheet));
|
return stylesheetFactory.newTemplates(xslt);
|
}
|
|
private void loadXMLDescriptors() throws IOException, MojoExecutionException {
|
final String parentPath = getXMLPackageDirectory();
|
if (isExtension) {
|
loadXMLDescriptorsFromFolder(parentPath);
|
return;
|
}
|
|
final URL url = getClass().getClassLoader().getResource(parentPath);
|
final String protocol = url.getProtocol();
|
if ("file".equals(protocol)) {
|
loadXMLDescriptorsFromFolder(parentPath);
|
} else if ("jar".equals(protocol)) {
|
loadXMLDescriptorsFromJar(parentPath, ((JarURLConnection) url.openConnection()).getJarFile());
|
} else {
|
final String errorMsg = "Impossible to read XML descriptors from path '" + parentPath + "'";
|
getLog().error(errorMsg);
|
throw new MojoExecutionException(errorMsg);
|
}
|
}
|
|
private void loadXMLDescriptorsFromFolder(final String parentPath) {
|
final File folder = new File(parentPath);
|
folder.listFiles(new FileFilter() {
|
@Override
|
public boolean accept(final File path) {
|
final String name = path.getName();
|
if (path.isFile() && name.endsWith(CONFIGURATION_FILE_SUFFIX)) {
|
final String key = name.substring(0, name.length() - CONFIGURATION_FILE_SUFFIX.length());
|
componentDescriptors.put(key, new StreamSourceFactory() {
|
@Override
|
public StreamSource newStreamSource() {
|
return new StreamSource(path);
|
}
|
});
|
}
|
return true; // Don't care about the result.
|
}
|
});
|
}
|
|
private void loadXMLDescriptorsFromJar(final String parentPath, final JarFile jar) throws IOException {
|
final Enumeration<JarEntry> entries = jar.entries();
|
|
while (entries.hasMoreElements()) {
|
final JarEntry entry = entries.nextElement();
|
final String name = entry.getName();
|
|
if (name.startsWith(parentPath) && name.endsWith(CONFIGURATION_FILE_SUFFIX)) {
|
final int startPos = name.lastIndexOf('/') + 1;
|
final int endPos = name.length() - CONFIGURATION_FILE_SUFFIX.length();
|
componentDescriptors.put(name.substring(startPos, endPos), new StreamSourceFactory() {
|
@Override
|
public StreamSource newStreamSource() throws IOException {
|
return new StreamSource(jar.getInputStream(entry));
|
}
|
});
|
}
|
}
|
}
|
}
|