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}