/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2013-2015 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 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;
/**
* 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, defaultValue = "true")
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();
loadXMLDescriptors();
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();
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 {
getLog().info("Loading XML descriptors...");
final String parentPath = getXMLPackageDirectory();
final String configFileName = "Configuration.xml";
if (isExtension) {
final File dir = new File(parentPath);
dir.listFiles(new FileFilter() {
@Override
public boolean accept(final File path) {
final String name = path.getName();
if (path.isFile() && name.endsWith(configFileName)) {
final String key = name.substring(0, name.length() - configFileName.length());
componentDescriptors.put(key, new StreamSourceFactory() {
@Override
public StreamSource newStreamSource() {
return new StreamSource(path);
}
});
}
return true; // Don't care about the result.
}
});
} else {
final URL dir = getClass().getClassLoader().getResource(parentPath);
final JarURLConnection connection = (JarURLConnection) dir.openConnection();
final JarFile jar = connection.getJarFile();
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(configFileName)) {
final int startPos = name.lastIndexOf('/') + 1;
final int endPos = name.length() - configFileName.length();
final String key = name.substring(startPos, endPos);
componentDescriptors.put(key, new StreamSourceFactory() {
@Override
public StreamSource newStreamSource() throws IOException {
return new StreamSource(jar.getInputStream(entry));
}
});
}
}
}
getLog().info("Found " + componentDescriptors.size() + " XML descriptors");
}
}