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 2015 ForgeRock AS. 025 */ 026package org.forgerock.opendj.maven.doc; 027 028import static org.forgerock.opendj.maven.doc.Utils.*; 029 030import org.apache.maven.plugin.AbstractMojo; 031import org.apache.maven.plugin.MojoExecutionException; 032import org.apache.maven.plugin.MojoFailureException; 033import org.apache.maven.plugins.annotations.LifecyclePhase; 034import org.apache.maven.plugins.annotations.Mojo; 035import org.apache.maven.plugins.annotations.Parameter; 036import org.apache.maven.plugins.annotations.ResolutionScope; 037import org.apache.maven.project.MavenProject; 038 039import java.io.BufferedReader; 040import java.io.ByteArrayInputStream; 041import java.io.File; 042import java.io.FileReader; 043import java.io.FileWriter; 044import java.io.IOException; 045import java.io.InputStream; 046import java.io.InputStreamReader; 047import java.net.URISyntaxException; 048import java.net.URLClassLoader; 049import java.nio.charset.Charset; 050import java.util.LinkedList; 051import java.util.List; 052import java.util.regex.Matcher; 053import java.util.regex.Pattern; 054 055/** 056 * Generate DocBook RefEntry source documents for command-line tools man pages. 057 */ 058@Mojo(name = "generate-refentry", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, 059 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 060public final class GenerateRefEntriesMojo extends AbstractMojo { 061 062 /** The Maven Project. */ 063 @Parameter(property = "project", required = true, readonly = true) 064 private MavenProject project; 065 066 /** Tools for which to generate RefEntry files. */ 067 @Parameter 068 private List<CommandLineTool> tools; 069 070 /** Where to write the RefEntry files. */ 071 @Parameter(required = true) 072 private File outputDir; 073 074 private static final String EOL = System.getProperty("line.separator"); 075 076 /** 077 * Writes a RefEntry file to the output directory for each tool. 078 * Files names correspond to script names: {@code man-<name>.xml}. 079 * 080 * @throws MojoExecutionException Encountered a problem writing a file. 081 * @throws MojoFailureException Failed to initialize effectively, 082 * or to write one or more RefEntry files. 083 */ 084 @Override 085 public void execute() throws MojoExecutionException, MojoFailureException { 086 if (!isOutputDirAvailable()) { 087 throw new MojoFailureException("Output directory " + outputDir.getPath() + " not available"); 088 } 089 090 // A Maven plugin classpath does not include project files. 091 // Prepare a ClassLoader capable of loading the command-line tools. 092 final URLClassLoader toolsClassLoader; 093 try { 094 toolsClassLoader = getRuntimeClassLoader(project, getLog()); 095 } catch (Exception e) { 096 throw new MojoExecutionException("Failed to get class loader.", e); 097 } 098 for (CommandLineTool tool : tools) { 099 if (tool.isEnabled()) { 100 generateManPageForTool(toolsClassLoader, tool); 101 } 102 } 103 } 104 105 /** 106 * Generate a RefEntry file to the output directory for a tool. 107 * The files name corresponds to the tool name: {@code man-<name>.xml}. 108 * @param toolsClassLoader The ClassLoader to run the tool. 109 * @param tool The tool to run in order to generate the page. 110 * @throws MojoExecutionException Failed to run the tool. 111 * @throws MojoFailureException Tool did not exit successfully. 112 */ 113 private void generateManPageForTool(final URLClassLoader toolsClassLoader, final CommandLineTool tool) 114 throws MojoExecutionException, MojoFailureException { 115 final File manPage = new File(outputDir, "man-" + tool.getName() + ".xml"); 116 final String toolScript = tool.getName(); 117 final String toolSects = pathsToXIncludes(tool.getTrailingSectionPaths()); 118 final String toolClass = tool.getApplication(); 119 List<String> commands = new LinkedList<>(); 120 commands.add(getJavaCommand()); 121 commands.addAll(getJavaArgs(toolScript, toolSects)); 122 commands.add("-classpath"); 123 try { 124 commands.add(getClassPath(toolsClassLoader)); 125 } catch (URISyntaxException e) { 126 throw new MojoExecutionException("Failed to set the classpath.", e); 127 } 128 commands.add(toolClass); 129 commands.add(getUsageArgument(toolScript)); 130 131 getLog().info("Writing man page: " + manPage.getPath()); 132 try { 133 // Tools tend to use System.exit() so run them as separate processes. 134 ProcessBuilder builder = new ProcessBuilder(commands); 135 Process process = builder.start(); 136 writeToFile(process.getInputStream(), manPage); 137 process.waitFor(); 138 final int result = process.exitValue(); 139 if (result != 0) { 140 final StringBuilder message = new StringBuilder(); 141 message.append("Failed to write page. Tool exit code: ").append(result).append(EOL) 142 .append("To debug the problem, run the following command and connect your IDE:").append(EOL); 143 commands.add(1, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000"); 144 for (String arg: commands) { 145 // Surround with quotes to handle trailing sections. 146 message.append("\"").append(arg).append("\"").append(' '); 147 } 148 message.append(EOL); 149 throw new MojoFailureException(message.toString()); 150 } 151 } catch (InterruptedException e) { 152 throw new MojoExecutionException(toolClass + " interrupted", e); 153 } catch (IOException e) { 154 throw new MojoExecutionException(toolClass + " not found", e); 155 } 156 157 if (tool.getName().equals("dsconfig")) { 158 try { 159 splitPage(manPage); 160 } catch (IOException e) { 161 throw new MojoExecutionException("Failed to split " + manPage.getName(), e); 162 } 163 } 164 } 165 166 /** 167 * Returns true if the output directory is available. 168 * Attempts to create the directory if it does not exist. 169 * @return True if the output directory is available. 170 */ 171 private boolean isOutputDirAvailable() { 172 return outputDir != null && (outputDir.exists() && outputDir.isDirectory() || outputDir.mkdirs()); 173 } 174 175 /** 176 * Returns the Java args for running a tool. 177 * @param scriptName The name of the tool. 178 * @param trailingSections The man page sections to Xinclude. 179 * @return The Java args for running a tool. 180 */ 181 private List<String> getJavaArgs(final String scriptName, final String trailingSections) { 182 List<String> args = new LinkedList<>(); 183 args.add("-Dorg.forgerock.opendj.gendoc=true"); 184 args.add("-Dorg.opends.server.ServerRoot=" + System.getProperty("java.io.tmpdir")); 185 args.add("-Dcom.forgerock.opendj.ldap.tools.scriptName=" + scriptName); 186 args.add("-Dorg.forgerock.opendj.gendoc.trailing=" + trailingSections + ""); 187 return args; 188 } 189 190 /** 191 * Translates relative paths to XML files into XInclude elements. 192 * 193 * @param paths Paths to XInclude'd files, relative to the RefEntry. 194 * @return String of XInclude elements corresponding to the paths. 195 */ 196 private String pathsToXIncludes(final List<String> paths) { 197 if (paths == null) { 198 return ""; 199 } 200 201 // Assume xmlns:xinclude="http://www.w3.org/2001/XInclude", 202 // as in the declaration of resources/templates/refEntry.ftl. 203 final String nameSpace = "xinclude"; 204 final StringBuilder result = new StringBuilder(); 205 for (String path : paths) { 206 result.append("<").append(nameSpace).append(":include href='").append(path).append("' />"); 207 } 208 return result.toString(); 209 } 210 211 /** 212 * Returns the usage argument. 213 * @param scriptName The name of the tool. 214 * @return The usage argument. 215 */ 216 private String getUsageArgument(final String scriptName) { 217 return scriptName.equals("dsjavaproperties") ? "-H" : "-?"; 218 } 219 220 /** 221 * Write the content of the input stream to the output file. 222 * @param input The input stream to write. 223 * @param output The file to write it to. 224 * @throws IOException Failed to write the content of the input stream. 225 */ 226 private void writeToFile(final InputStream input, final File output) throws IOException { 227 try (FileWriter writer = new FileWriter(output)) { 228 BufferedReader reader = new BufferedReader(new InputStreamReader(input)); 229 String line; 230 while ((line = reader.readLine()) != null) { 231 writer.write(line); 232 writer.write(EOL); 233 } 234 } 235 } 236 237 /** 238 * Splits the content of a single man page into multiple pages. 239 * <br> 240 * RefEntry elements must be separated with a marker: 241 * {@code @@@scriptName + "-" + subCommand.getName() + @@@}. 242 * 243 * @param page The page to split. 244 * @throws IOException Failed to split the page. 245 */ 246 private void splitPage(final File page) throws IOException { 247 // Read from a copy of the page. 248 final File pageCopy = new File(page.getPath() + ".tmp"); 249 copyFile(page, pageCopy); 250 try (final BufferedReader reader = new BufferedReader(new FileReader(pageCopy))) { 251 // Write first to the page, then to pages named according to marker values. 252 File output = page; 253 getLog().info("Rewriting man page: " + page.getPath()); 254 final Pattern marker = Pattern.compile("@@@(.+?)@@@"); 255 final StringBuilder builder = new StringBuilder(); 256 String line; 257 while ((line = reader.readLine()) != null) { 258 final Matcher matcher = marker.matcher(line); 259 if (matcher.find()) { 260 writeToFile(builder.toString(), output); 261 builder.setLength(0); 262 output = new File(page.getParentFile(), "man-" + matcher.group(1) + ".xml"); 263 getLog().info("Writing man page: " + output.getPath()); 264 } else { 265 builder.append(line).append(System.getProperty("line.separator")); 266 } 267 } 268 writeToFile(builder.toString(), output); 269 if (!pageCopy.delete()) { 270 throw new IOException("Failed to delete " + pageCopy.getName()); 271 } 272 } 273 } 274 275 /** 276 * Writes the content of the input to the output file. 277 * @param input The UTF-8 input to write. 278 * @param output The file to write it to. 279 * @throws IOException Failed to write the content of the input. 280 */ 281 private void writeToFile(final String input, final File output) throws IOException { 282 InputStream is = new ByteArrayInputStream(input.getBytes(Charset.forName("UTF-8"))); 283 writeToFile(is, output); 284 } 285}