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