/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS */ package org.forgerock.opendj.maven; import static org.apache.maven.plugins.annotations.LifecyclePhase.*; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.forgerock.util.Utils; /** * Generates xml files containing representations of messages found in * properties files. *

* There is a single goal that generates xml files. *

*/ @Mojo(name = "generate-xml-messages-doc", defaultPhase = PRE_SITE) public class GenerateMessageFileMojo extends AbstractMojo { /** * The Maven Project. */ @Parameter(property = "project", readonly = true, required = true) private MavenProject project; /** * The path to the directory containing the message properties files. */ @Parameter(required = true) private String messagesDirectory; /** * The path to the directory where xml reference files should be written. * This path must be relative to ${project.build.directory}. */ @Parameter(required = true) private String outputDirectory; /** * A list which contains all file names, the extension is not needed. */ @Parameter(required = true) private List messageFileNames; /** * The path and file name of the log message reference file path which will * be copied in the output directory with generated log reference files. */ @Parameter(required = true) private String logMessageReferenceFilePath; /** * If the plugin is supposed to overwrite existing generated xml files. */ @Parameter(required = true, defaultValue = "false") private boolean overwrite; /** The end-of-line character for this platform. */ public static final String EOL = System.getProperty("line.separator"); /** * The registry filename is the result of the concatenation of the location * of where the source are generated, the package name and the * DESCRIPTORS_REG value. */ private static String registryFileName; /** * One-line descriptions for log reference categories. */ private static final HashMap CATEGORY_DESCRIPTIONS = new HashMap(); static { CATEGORY_DESCRIPTIONS.put("ACCESS_CONTROL", "Access Control."); CATEGORY_DESCRIPTIONS.put("ADMIN", "the administration framework."); CATEGORY_DESCRIPTIONS.put("ADMIN_TOOL", "the tool like the offline" + " installer and uninstaller."); CATEGORY_DESCRIPTIONS.put("BACKEND", "generic backends."); CATEGORY_DESCRIPTIONS.put("CONFIG", "configuration handling."); CATEGORY_DESCRIPTIONS.put("CORE", "the core server."); CATEGORY_DESCRIPTIONS.put("DSCONFIG", "the dsconfig administration tool."); CATEGORY_DESCRIPTIONS.put("EXTENSIONS", "server extensions for example," + " extended operations, SASL mechanisms, password storage" + " schemes, password validators, and so on)."); CATEGORY_DESCRIPTIONS.put("JEB", "the JE backend."); CATEGORY_DESCRIPTIONS.put("LOG", "the server loggers."); CATEGORY_DESCRIPTIONS.put("PLUGIN", "plugin processing."); CATEGORY_DESCRIPTIONS.put("PROTOCOL", "connection and protocol handling" + " (for example, ASN.1 and LDAP)."); CATEGORY_DESCRIPTIONS.put("QUICKSETUP", "quicksetup tools."); CATEGORY_DESCRIPTIONS.put("RUNTIME_INFORMATION", "the runtime" + " information."); CATEGORY_DESCRIPTIONS.put("SCHEMA", "the server schema elements."); CATEGORY_DESCRIPTIONS.put("SYNC", "replication."); CATEGORY_DESCRIPTIONS.put("TASK", "tasks."); CATEGORY_DESCRIPTIONS.put("THIRD_PARTY", "third-party (including" + " user-defined) modules."); CATEGORY_DESCRIPTIONS.put("TOOLS", "tools."); CATEGORY_DESCRIPTIONS.put("USER_DEFINED", "user-defined modules."); CATEGORY_DESCRIPTIONS.put("UTIL", "the general server utilities."); CATEGORY_DESCRIPTIONS.put("VERSION", "version information."); } private static final String DESCRIPTORS_REG = "descriptors.reg"; /** Message giving formatting rules for string keys. */ public static final String KEY_FORM_MSG = ".\n\nOpenDJ message property keys must be of the form\n\n" + "\t\'[CATEGORY]_[SEVERITY]_[DESCRIPTION]_[ORDINAL]\'\n\n"; private static final String ERROR_SEVERITY_IDENTIFIER_STRING = "ERR_"; private static final String ERROR_SEVERITY_PRINTABLE = "ERROR"; /** * Represents a log reference entry for an individual message. */ private static class MessageRefEntry implements Comparable { private Integer ordinal; private String xmlId; private String formatString; /** * Build log reference entry for an log message. */ public MessageRefEntry(final String msgPropKey, final Integer ordinal, final String formatString) { this.formatString = formatString; this.ordinal = ordinal; xmlId = getXmlId(msgPropKey); } private String getXmlId(final String messagePropertyKey) { // XML IDs must be unique, and must begin with a letter ([A-Za-z]) // and may be followed by any number of letters, digits ([0-9]), // hyphens ("-"), underscores ("_"), colons (":"), and periods // ("."). final String invalidChars = "[^A-Za-z0-9\\-_:\\.]"; return messagePropertyKey.replaceAll(invalidChars, "-"); } /** * Return a DocBook XML <varlistentry> of this log reference * entry. This implementation copies the message string verbatim, and * does not interpret format specifiers. * * @return DocBook XML <varlistentry>. */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(" ").append(EOL); if (ordinal != null) { builder.append(" ID: ").append(ordinal).append("").append(EOL); } builder.append(" ").append(EOL); builder.append(" Severity: ").append(ERROR_SEVERITY_PRINTABLE).append("").append(EOL); builder.append(" Message: ").append(formatString).append("").append(EOL); builder.append(" ").append(EOL); builder.append(" ").append(EOL); return builder.toString(); } /** * Calls {@link #toString()}. */ public String toXML() { return toString(); } /** * Compare message entries by unique identifier. * * @return See {@link java.lang.Comparable#compareTo(Object)}. */ @Override public int compareTo(MessageRefEntry mre) { if (this.ordinal != null && mre.ordinal != null) { return this.ordinal.compareTo(mre.ordinal); } return 0; } } /** Represents a log reference list of messages for a category. */ private static class MessageRefCategory { private String category; private TreeSet messages; MessageRefCategory(final String category, final TreeSet messages) { this.category = category; this.messages = messages; } /** * Return a DocBook XML <variablelist> of this log reference * category. * * @return DocBook XML <variablelist> */ @Override public String toString() { StringBuilder entries = new StringBuilder(); for (MessageRefEntry entry : messages) { entries.append(entry.toXML()); } return getVariablelistHead() + entries + getVariablelistTail(); } /** * Calls {@link #toString()}. */ public String toXML() { return toString(); } private String getXMLPreamble() { DateFormat df = new SimpleDateFormat("yyyy"); Date now = new Date(); String year = df.format(now); return "" + EOL + "" + EOL; } private String getBaseElementAttrs() { return "xmlns='http://docbook.org/ns/docbook'" + " version='5.0' xml:lang='en'" + " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" + " xsi:schemaLocation='http://docbook.org/ns/docbook" + " http://docbook.org/xml/5.0/xsd/docbook.xsd'" + " xmlns:xlink='http://www.w3.org/1999/xlink'" + " xmlns:xinclude='http://www.w3.org/2001/XInclude'"; } private String getVariablelistHead() { return getXMLPreamble() + " " + EOL + " Log Message Category: " + category + "" + EOL; } private String getVariablelistTail() { return " " + EOL; } } private static class MessagePropertyKey implements Comparable { private String description; private Integer ordinal; /** * Creates a message property key from a string value. * * @param key * from properties file * @return MessagePropertyKey created from string */ public static MessagePropertyKey parseString(String key) { int li = key.lastIndexOf("_"); if (li == -1) { throw new IllegalArgumentException("Incorrectly formatted key " + key); } final String description = key.substring(0, li).toUpperCase(); Integer ordinal = null; try { String ordString = key.substring(li + 1); ordinal = Integer.parseInt(ordString); } catch (Exception nfe) { // Ignore exception, the message has no ordinal. } return new MessagePropertyKey(description, ordinal); } /** * Creates a parameterized instance. * * @param description * of this key * @param ordinal * of this key */ public MessagePropertyKey(String description, Integer ordinal) { this.description = description; this.ordinal = ordinal; } /** * Gets the ordinal of this key. * * @return ordinal of this key */ public Integer getOrdinal() { return this.ordinal; } /** {@inheritDoc} */ @Override public String toString() { if (ordinal != null) { return description + "_" + ordinal; } return description; } /** {@inheritDoc} */ @Override public int compareTo(MessagePropertyKey k) { if (ordinal == k.ordinal) { return description.compareTo(k.description); } else { return ordinal.compareTo(k.ordinal); } } } /** * For maven exec plugin execution. Generates for all included message files * (sample.properties), a xml log ref file (log-ref-sample.xml) * * @throws MojoExecutionException * if a problem occurs * @throws MojoFailureException * if a problem occurs */ @Override public void execute() throws MojoExecutionException, MojoFailureException { String projectBuildDir = project.getBuild().getDirectory(); if (!outputDirectory.contains(projectBuildDir)) { String errorMsg = String.format("outputDirectory parameter (%s) must be included " + "in ${project.build.directory} (%s)", outputDirectory, projectBuildDir); getLog().error(errorMsg); throw new MojoExecutionException(errorMsg); } for (String messageFileName : messageFileNames) { File source = new File(messagesDirectory, messageFileName + ".properties"); File dest = new File(outputDirectory, "log-ref-" + messageFileName.replace("_", "-") + ".xml"); try { generateLogReferenceFile(source, dest, messageFileName.toUpperCase()); } catch (MojoExecutionException e) { getLog().error("Impossible to generate " + dest.getAbsolutePath() + ": " + e.getMessage()); throw e; } } copyLogMessageReferenceFile(); } private void generateLogReferenceFile(File source, File dest, String globalCategory) throws MojoExecutionException { PrintWriter destWriter = null; try { // Decide whether to generate messages based on modification times // and print status messages. if (!source.exists()) { throw new Exception("file " + source.getName() + " does not exist"); } if (!isOverwriteNeeded(source, dest)) { return; } destWriter = new PrintWriter(dest, "UTF-8"); Properties properties = new Properties(); properties.load(new FileInputStream(source)); Map errorMessages = loadErrorProperties(properties); TreeSet messageRefEntries = new TreeSet(); Set usedOrdinals = new HashSet(); for (MessagePropertyKey msgKey : errorMessages.keySet()) { String formatString = errorMessages.get(msgKey).replaceAll("<", "<"); Integer ordinal = msgKey.getOrdinal(); if (ordinal != null && usedOrdinals.contains(ordinal)) { throw new Exception("The ordinal value \'" + ordinal + "\' in key " + msgKey + " has been previously defined in " + source + KEY_FORM_MSG); } usedOrdinals.add(ordinal); messageRefEntries.add(new MessageRefEntry(msgKey.toString(), ordinal, formatString)); } destWriter.println(messageRefEntries.isEmpty() ? "" : new MessageRefCategory(globalCategory, messageRefEntries).toXML()); getLog().info(dest.getPath() + " has been successfully generated"); getLog().debug("Message Generated: " + errorMessages.size()); } catch (Exception e) { // Delete malformed file. if (dest.exists()) { dest.deleteOnExit(); } throw new MojoExecutionException(e.getMessage(), e); } finally { Utils.closeSilently(destWriter); } } private Map loadErrorProperties(Properties properties) throws Exception { Map errorMessage = new TreeMap(); for (Object propO : properties.keySet()) { String propKey = propO.toString(); try { // Document only ERROR messages. if (propKey.startsWith(ERROR_SEVERITY_IDENTIFIER_STRING)) { MessagePropertyKey key = MessagePropertyKey.parseString(propKey); String formatString = properties.getProperty(propKey); errorMessage.put(key, formatString); } } catch (IllegalArgumentException iae) { throw new Exception("invalid property key " + propKey + ": " + iae.getMessage() + KEY_FORM_MSG, iae); } } return errorMessage; } private boolean isOverwriteNeeded(File source, File dest) { boolean needsOverwrite = this.overwrite || source.lastModified() > dest.lastModified(); if (dest.exists() && needsOverwrite) { dest.delete(); getLog().info("Regenerating " + dest.getName() + " from " + source.getName()); } else if (dest.exists() && !needsOverwrite) { // Fail fast - nothing to do. getLog().info(dest.getName() + " is up to date"); return false; } else { File javaGenDir = dest.getParentFile(); if (!javaGenDir.exists()) { javaGenDir.mkdirs(); } } return true; } private void copyLogMessageReferenceFile() throws MojoExecutionException { File msgReferenceSourceFile = new File(logMessageReferenceFilePath); File msgReferenceDestFile = new File(outputDirectory, msgReferenceSourceFile.getName()); if (!isOverwriteNeeded(msgReferenceSourceFile, msgReferenceDestFile)) { return; } InputStream input = null; OutputStream output = null; try { input = new FileInputStream(msgReferenceSourceFile); output = new FileOutputStream(msgReferenceDestFile); byte[] buf = new byte[1024]; int bytesRead; while ((bytesRead = input.read(buf)) > 0) { output.write(buf, 0, bytesRead); } getLog().info("log message reference file has been successfully generated"); } catch (Exception e) { throw new MojoExecutionException("Impossible to copy log reference message file into output directory: " + e.getMessage(), e); } finally { Utils.closeSilently(input, output); } } /** * Sets the file that will be generated containing declarations of messages * from source. * * @param dest * File destination * @throws Exception * If a problem occurs */ public void checkDestJava(File dest) throws Exception { File descriptorsRegFile = new File(dest.getParentFile(), DESCRIPTORS_REG); if (registryFileName != null) { // if REGISTRY_FILE_NAME is already set, ensure that we computed the // same one File prevDescriptorsRegFile = new File(registryFileName); if (!prevDescriptorsRegFile.equals(descriptorsRegFile)) { throw new Exception("Error processing " + dest + ": all messages must be located in the same package thus " + "name of the source file should be " + new File(prevDescriptorsRegFile.getParent(), dest.getName())); } } else { registryFileName = descriptorsRegFile.getCanonicalPath(); } } }