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}