001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2013-2016 ForgeRock AS.
015 */
016package org.forgerock.opendj.maven;
017
018import static org.apache.maven.plugins.annotations.LifecyclePhase.*;
019import static org.apache.maven.plugins.annotations.ResolutionScope.*;
020
021import java.io.File;
022import java.io.FileFilter;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.net.JarURLConnection;
026import java.net.URL;
027import java.util.Enumeration;
028import java.util.LinkedHashMap;
029import java.util.LinkedList;
030import java.util.Map;
031import java.util.Queue;
032import java.util.concurrent.Callable;
033import java.util.concurrent.ExecutorService;
034import java.util.concurrent.Executors;
035import java.util.concurrent.Future;
036import java.util.jar.JarEntry;
037import java.util.jar.JarFile;
038
039import javax.xml.transform.Source;
040import javax.xml.transform.Templates;
041import javax.xml.transform.Transformer;
042import javax.xml.transform.TransformerConfigurationException;
043import javax.xml.transform.TransformerException;
044import javax.xml.transform.TransformerFactory;
045import javax.xml.transform.URIResolver;
046import javax.xml.transform.stream.StreamResult;
047import javax.xml.transform.stream.StreamSource;
048
049import org.apache.maven.model.Resource;
050import org.apache.maven.plugin.AbstractMojo;
051import org.apache.maven.plugin.MojoExecutionException;
052import org.apache.maven.plugins.annotations.Mojo;
053import org.apache.maven.plugins.annotations.Parameter;
054import org.apache.maven.project.MavenProject;
055
056/**
057 * Generate configuration classes from XML definition files for OpenDJ server.
058 * <p>
059 * There is a single goal that generate java sources, manifest files, I18N
060 * messages and cli/ldap profiles. Resources will be looked for in the following
061 * places depending on whether the plugin is executing for the core config or an
062 * extension:
063 * <table border="1">
064 * <tr>
065 * <th></th>
066 * <th>Location</th>
067 * </tr>
068 * <tr>
069 * <th align="left">XSLT stylesheets</th>
070 * <td>Internal: /config/stylesheets</td>
071 * </tr>
072 * <tr>
073 * <th align="left">XML core definitions</th>
074 * <td>Internal: /config/xml</td>
075 * </tr>
076 * <tr>
077 * <th align="left">XML extension definitions</th>
078 * <td>${basedir}/src/main/java</td>
079 * </tr>
080 * <tr>
081 * <th align="left">Generated Java APIs</th>
082 * <td>${project.build.directory}/generated-sources/config</td>
083 * </tr>
084 * <tr>
085 * <th align="left">Generated I18N messages</th>
086 * <td>${project.build.outputDirectory}/config/messages</td>
087 * </tr>
088 * <tr>
089 * <th align="left">Generated profiles</th>
090 * <td>${project.build.outputDirectory}/config/profiles/${profile}</td>
091 * </tr>
092 * <tr>
093 * <th align="left">Generated manifest</th>
094 * <td>${project.build.outputDirectory}/META-INF/services/org.forgerock.opendj.
095 * config.AbstractManagedObjectDefinition</td>
096 * </tr>
097 * </table>
098 */
099@Mojo(name = "generate-config", defaultPhase = GENERATE_SOURCES, requiresDependencyResolution = COMPILE_PLUS_RUNTIME)
100public final class GenerateConfigMojo extends AbstractMojo {
101
102    private static final String CONFIGURATION_FILE_SUFFIX = "Configuration.xml";
103
104    private interface StreamSourceFactory {
105        StreamSource newStreamSource() throws IOException;
106    }
107
108    /**
109     * The Maven Project.
110     */
111    @Parameter(required = true, readonly = true, property = "project")
112    private MavenProject project;
113
114    /**
115     * Package name for which artifacts are generated.
116     * <p>
117     * This relative path is used to locate xml definition files and to locate
118     * generated artifacts.
119     */
120    @Parameter(required = true)
121    private String packageName;
122
123    /**
124     * {@code true} if this plugin should be used to generate classes
125     * for extended configuration (e.g OpenDJ plugins).
126     * <p>
127     * If not specified, OpenDJ configuration classes will be generated.
128     */
129    @Parameter(required = true, defaultValue = "false")
130    private Boolean isExtension;
131
132    private final Map<String, StreamSourceFactory> componentDescriptors = new LinkedHashMap<>();
133    private TransformerFactory stylesheetFactory;
134    private Templates stylesheetMetaJava;
135    private Templates stylesheetServerJava;
136    private Templates stylesheetClientJava;
137    private Templates stylesheetMetaPackageInfo;
138    private Templates stylesheetServerPackageInfo;
139    private Templates stylesheetClientPackageInfo;
140    private Templates stylesheetProfileLDAP;
141    private Templates stylesheetProfileCLI;
142    private Templates stylesheetMessages;
143    private Templates stylesheetManifest;
144    private final Queue<Future<?>> tasks = new LinkedList<>();
145
146    private final URIResolver resolver = new URIResolver() {
147
148        @Override
149        public synchronized Source resolve(final String href, final String base)
150                throws TransformerException {
151            if (href.endsWith(".xsl")) {
152                final String stylesheet;
153                if (href.startsWith("../")) {
154                    stylesheet = "/config/stylesheets/" + href.substring(3);
155                } else {
156                    stylesheet = "/config/stylesheets/" + href;
157                }
158                getLog().debug("#### Resolved stylesheet " + href + " to " + stylesheet);
159                return new StreamSource(getClass().getResourceAsStream(stylesheet));
160            } else if (href.endsWith(".xml")) {
161                if (href.startsWith("org/forgerock/opendj/server/config/")) {
162                    final String coreXML = "/config/xml/" + href;
163                    getLog().debug("#### Resolved core XML definition " + href + " to " + coreXML);
164                    return new StreamSource(getClass().getResourceAsStream(coreXML));
165                } else {
166                    final String extXML = getXMLDirectory() + "/" + href;
167                    getLog().debug(
168                            "#### Resolved extension XML definition " + href + " to " + extXML);
169                    return new StreamSource(new File(extXML));
170                }
171            } else {
172                throw new TransformerException("Unable to resolve URI " + href);
173            }
174        }
175    };
176
177    @Override
178    public void execute() throws MojoExecutionException {
179        if (getPackagePath() == null) {
180            throw new MojoExecutionException("<packagePath> must be set.");
181        } else if (!isXMLPackageDirectoryValid()) {
182            throw new MojoExecutionException("The XML definition directory \""
183                    + getXMLPackageDirectory() + "\" does not exist.");
184        } else if (getClass().getResource(getStylesheetDirectory()) == null) {
185            throw new MojoExecutionException("The XSLT stylesheet directory \""
186                    + getStylesheetDirectory() + "\" does not exist.");
187        }
188
189        // Validate and transform.
190        try {
191            initializeStylesheets();
192            getLog().info("Loading XML descriptors...");
193            loadXMLDescriptors();
194            getLog().info("Found " + componentDescriptors.size() + " XML descriptors");
195            executeValidateXMLDefinitions();
196            executeTransformXMLDefinitions();
197            getLog().info("Adding source directory \"" + getGeneratedSourcesDirectory() + "\" to build path...");
198            project.addCompileSourceRoot(getGeneratedSourcesDirectory());
199            project.addResource(getGeneratedMavenResources());
200        } catch (final Exception e) {
201            throw new MojoExecutionException("XSLT configuration transformation failed", e);
202        }
203    }
204
205    private Resource getGeneratedMavenResources() {
206        final String[] generatedResourcesRelativePath =
207                new String[] { "/META-INF/services/**", "/config/**/*.properties" };
208        final Resource resources = new Resource();
209        resources.setDirectory(getGeneratedResourcesDirectory());
210        for (final String generatedResourceRelativePath : generatedResourcesRelativePath) {
211            resources.addInclude(generatedResourceRelativePath);
212            getLog().info("Adding resource \"" + getGeneratedResourcesDirectory() + generatedResourceRelativePath
213                    + " to resource path...");
214        }
215
216        return resources;
217    }
218
219    private void createTransformTask(final StreamSourceFactory inputFactory, final StreamResult output,
220            final Templates stylesheet, final ExecutorService executor, final String... parameters)
221            throws Exception {
222        final Future<Void> future = executor.submit(new Callable<Void>() {
223            @Override
224            public Void call() throws Exception {
225                final Transformer transformer = stylesheet.newTransformer();
226                transformer.setURIResolver(resolver);
227                for (int i = 0; i < parameters.length; i += 2) {
228                    transformer.setParameter(parameters[i], parameters[i + 1]);
229                }
230                transformer.transform(inputFactory.newStreamSource(), output);
231                return null;
232            }
233        });
234        tasks.add(future);
235    }
236
237    private void createTransformTask(final StreamSourceFactory inputFactory,
238            final String outputFileName, final Templates stylesheet,
239            final ExecutorService executor, final String... parameters) throws Exception {
240        final File outputFile = new File(outputFileName);
241        outputFile.getParentFile().mkdirs();
242        final StreamResult output = new StreamResult(outputFile);
243        createTransformTask(inputFactory, output, stylesheet, executor, parameters);
244    }
245
246    private void executeTransformXMLDefinitions() throws Exception {
247        getLog().info("Transforming XML definitions...");
248
249        /*
250         * Restrict the size of the thread pool in order to throttle
251         * creation of transformers and ZIP input streams and prevent potential
252         * OOME.
253         */
254        final ExecutorService parallelExecutor = Executors.newFixedThreadPool(16);
255
256        /*
257         * The manifest is a single file containing the concatenated output of
258         * many transformations. Therefore we must ensure that output is
259         * serialized by using a single threaded executor.
260         */
261        final ExecutorService sequentialExecutor = Executors.newSingleThreadExecutor();
262        final File manifestFile = new File(getGeneratedManifestFile());
263        manifestFile.getParentFile().mkdirs();
264        final FileOutputStream manifestFileOutputStream = new FileOutputStream(manifestFile);
265        final StreamResult manifest = new StreamResult(manifestFileOutputStream);
266        try {
267            /*
268             * Generate Java classes and resources for each XML definition.
269             */
270            final String javaDir = getGeneratedSourcesDirectory() + "/" + getPackagePath() + "/";
271            final String metaDir = javaDir + "meta/";
272            final String serverDir = javaDir + "server/";
273            final String clientDir = javaDir + "client/";
274            final String ldapProfileDir =
275                    getGeneratedProfilesDirectory("ldap") + "/" + getPackagePath() + "/meta/";
276            final String cliProfileDir =
277                    getGeneratedProfilesDirectory("cli") + "/" + getPackagePath() + "/meta/";
278            final String i18nDir =
279                    getGeneratedMessagesDirectory() + "/" + getPackagePath() + "/meta/";
280
281            for (final Map.Entry<String, StreamSourceFactory> entry : componentDescriptors
282                    .entrySet()) {
283                final String meta = metaDir + entry.getKey() + "CfgDefn.java";
284                createTransformTask(entry.getValue(), meta, stylesheetMetaJava, parallelExecutor);
285
286                final String server = serverDir + entry.getKey() + "Cfg.java";
287                createTransformTask(entry.getValue(), server, stylesheetServerJava,
288                        parallelExecutor);
289
290                final String client = clientDir + entry.getKey() + "CfgClient.java";
291                createTransformTask(entry.getValue(), client, stylesheetClientJava,
292                        parallelExecutor);
293
294                final String ldap = ldapProfileDir + entry.getKey() + "CfgDefn.properties";
295                createTransformTask(entry.getValue(), ldap, stylesheetProfileLDAP, parallelExecutor);
296
297                final String cli = cliProfileDir + entry.getKey() + "CfgDefn.properties";
298                createTransformTask(entry.getValue(), cli, stylesheetProfileCLI, parallelExecutor);
299
300                final String i18n = i18nDir + entry.getKey() + "CfgDefn.properties";
301                createTransformTask(entry.getValue(), i18n, stylesheetMessages, parallelExecutor);
302
303                createTransformTask(entry.getValue(), manifest, stylesheetManifest,
304                        sequentialExecutor);
305            }
306
307            // Generate package-info.java files.
308            final Map<String, Templates> profileMap = new LinkedHashMap<>();
309            profileMap.put("meta", stylesheetMetaPackageInfo);
310            profileMap.put("server", stylesheetServerPackageInfo);
311            profileMap.put("client", stylesheetClientPackageInfo);
312            for (final Map.Entry<String, Templates> entry : profileMap.entrySet()) {
313                final StreamSourceFactory sourceFactory = new StreamSourceFactory() {
314                    @Override
315                    public StreamSource newStreamSource() throws IOException {
316                        if (isExtension) {
317                            return new StreamSource(new File(getXMLPackageDirectory()
318                                    + "/Package.xml"));
319                        } else {
320                            return new StreamSource(getClass().getResourceAsStream(
321                                    "/" + getXMLPackageDirectory() + "/Package.xml"));
322                        }
323                    }
324                };
325                final String profile = javaDir + "/" + entry.getKey() + "/package-info.java";
326                createTransformTask(sourceFactory, profile, entry.getValue(), parallelExecutor,
327                        "type", entry.getKey());
328            }
329
330            /*
331             * Wait for all transformations to complete and cleanup. Remove the
332             * completed tasks from the list as we go in order to free up
333             * memory.
334             */
335            for (Future<?> task = tasks.poll(); task != null; task = tasks.poll()) {
336                task.get();
337            }
338        } finally {
339            parallelExecutor.shutdown();
340            sequentialExecutor.shutdown();
341            manifestFileOutputStream.close();
342        }
343    }
344
345    private void executeValidateXMLDefinitions() {
346        // TODO:
347        getLog().info("Validating XML definitions...");
348    }
349
350    private String getBaseDir() {
351        return project.getBasedir().toString();
352    }
353
354    private String getGeneratedResourcesDirectory() {
355        return project.getBuild().getDirectory() + "/generated-resources";
356    }
357
358    private String getGeneratedManifestFile() {
359        return getGeneratedResourcesDirectory()
360                + "/META-INF/services/org.forgerock.opendj.config.AbstractManagedObjectDefinition";
361    }
362
363    private String getGeneratedMessagesDirectory() {
364        return getGeneratedResourcesDirectory() + "/config/messages";
365    }
366
367    private String getGeneratedProfilesDirectory(final String profileName) {
368        return getGeneratedResourcesDirectory() + "/config/profiles/" + profileName;
369    }
370
371    private String getGeneratedSourcesDirectory() {
372        return project.getBuild().getDirectory() + "/generated-sources/config";
373    }
374
375    private String getPackagePath() {
376        return packageName.replace('.', '/');
377    }
378
379    private String getStylesheetDirectory() {
380        return "/config/stylesheets";
381    }
382
383    private String getXMLDirectory() {
384        if (isExtension) {
385            return getBaseDir() + "/src/main/java";
386        } else {
387            return "config/xml";
388        }
389    }
390
391    private String getXMLPackageDirectory() {
392        return getXMLDirectory() + "/" + getPackagePath();
393    }
394
395    private void initializeStylesheets() throws TransformerConfigurationException {
396        getLog().info("Loading XSLT stylesheets...");
397        stylesheetFactory = TransformerFactory.newInstance();
398        stylesheetFactory.setURIResolver(resolver);
399        stylesheetMetaJava = loadStylesheet("metaMO.xsl");
400        stylesheetMetaPackageInfo = loadStylesheet("package-info.xsl");
401        stylesheetServerJava = loadStylesheet("serverMO.xsl");
402        stylesheetServerPackageInfo = loadStylesheet("package-info.xsl");
403        stylesheetClientJava = loadStylesheet("clientMO.xsl");
404        stylesheetClientPackageInfo = loadStylesheet("package-info.xsl");
405        stylesheetProfileLDAP = loadStylesheet("ldapMOProfile.xsl");
406        stylesheetProfileCLI = loadStylesheet("cliMOProfile.xsl");
407        stylesheetMessages = loadStylesheet("messagesMO.xsl");
408        stylesheetManifest = loadStylesheet("manifestMO.xsl");
409    }
410
411    private boolean isXMLPackageDirectoryValid() {
412        // Not an extension, so always valid.
413        return !isExtension
414            || new File(getXMLPackageDirectory()).isDirectory();
415    }
416
417    private Templates loadStylesheet(final String stylesheet)
418            throws TransformerConfigurationException {
419        final Source xslt =
420                new StreamSource(getClass().getResourceAsStream(
421                        getStylesheetDirectory() + "/" + stylesheet));
422        return stylesheetFactory.newTemplates(xslt);
423    }
424
425    private void loadXMLDescriptors() throws IOException, MojoExecutionException {
426        final String parentPath = getXMLPackageDirectory();
427        if (isExtension) {
428            loadXMLDescriptorsFromFolder(parentPath);
429            return;
430        }
431
432        final URL url = getClass().getClassLoader().getResource(parentPath);
433        final String protocol = url.getProtocol();
434        if ("file".equals(protocol)) {
435            loadXMLDescriptorsFromFolder(parentPath);
436        } else if ("jar".equals(protocol)) {
437            loadXMLDescriptorsFromJar(parentPath, ((JarURLConnection) url.openConnection()).getJarFile());
438        } else {
439            final String errorMsg = "Impossible to read XML descriptors from path '" + parentPath + "'";
440            getLog().error(errorMsg);
441            throw new MojoExecutionException(errorMsg);
442        }
443    }
444
445    private void loadXMLDescriptorsFromFolder(final String parentPath) {
446        final File folder = new File(parentPath);
447        folder.listFiles(new FileFilter() {
448            @Override
449            public boolean accept(final File path) {
450                final String name = path.getName();
451                if (path.isFile() && name.endsWith(CONFIGURATION_FILE_SUFFIX)) {
452                    final String key = name.substring(0, name.length() - CONFIGURATION_FILE_SUFFIX.length());
453                    componentDescriptors.put(key, new StreamSourceFactory() {
454                        @Override
455                        public StreamSource newStreamSource() {
456                            return new StreamSource(path);
457                        }
458                    });
459                }
460                return true; // Don't care about the result.
461            }
462        });
463    }
464
465    private void loadXMLDescriptorsFromJar(final String parentPath, final JarFile jar) throws IOException {
466        final Enumeration<JarEntry> entries = jar.entries();
467
468        while (entries.hasMoreElements()) {
469            final JarEntry entry = entries.nextElement();
470            final String name = entry.getName();
471
472            if (name.startsWith(parentPath) && name.endsWith(CONFIGURATION_FILE_SUFFIX)) {
473                final int startPos = name.lastIndexOf('/') + 1;
474                final int endPos = name.length() - CONFIGURATION_FILE_SUFFIX.length();
475                componentDescriptors.put(name.substring(startPos, endPos), new StreamSourceFactory() {
476                    @Override
477                    public StreamSource newStreamSource() throws IOException {
478                        return new StreamSource(jar.getInputStream(entry));
479                    }
480                });
481            }
482        }
483    }
484}