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