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}