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-&lt;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-&lt;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}