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 2008-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS. 026 */ 027package org.forgerock.opendj.maven.doc; 028 029import static org.apache.maven.plugins.annotations.LifecyclePhase.*; 030import static org.forgerock.opendj.maven.doc.DocsMessages.*; 031import static org.forgerock.util.Utils.*; 032 033import java.io.File; 034import java.io.FileInputStream; 035import java.io.IOException; 036import java.io.PrintWriter; 037import java.io.Writer; 038import java.text.SimpleDateFormat; 039import java.util.Date; 040import java.util.HashMap; 041import java.util.HashSet; 042import java.util.LinkedList; 043import java.util.List; 044import java.util.Map; 045import java.util.Properties; 046import java.util.Set; 047import java.util.TreeMap; 048import java.util.TreeSet; 049 050import freemarker.template.Configuration; 051import freemarker.template.TemplateException; 052import freemarker.template.TemplateExceptionHandler; 053import org.apache.maven.plugin.AbstractMojo; 054import org.apache.maven.plugin.MojoExecutionException; 055import org.apache.maven.plugin.MojoFailureException; 056import org.apache.maven.plugins.annotations.Mojo; 057import org.apache.maven.plugins.annotations.Parameter; 058import org.apache.maven.project.MavenProject; 059import org.forgerock.i18n.LocalizableMessage; 060 061/** 062 * Generates an XML file of log messages found in properties files. 063 */ 064@Mojo(name = "generate-xml-messages-doc", defaultPhase = PRE_SITE) 065public class GenerateMessageFileMojo extends AbstractMojo { 066 067 /** 068 * The Maven Project. 069 */ 070 @Parameter(property = "project", readonly = true, required = true) 071 private MavenProject project; 072 073 /** 074 * The tag of the locale for which to generate the documentation. 075 */ 076 @Parameter(defaultValue = "en") 077 private String locale; 078 079 /** 080 * The path to the directory containing the message properties files. 081 */ 082 @Parameter(required = true) 083 private String messagesDirectory; 084 085 /** 086 * The path to the directory where the XML file should be written. 087 * This path must be relative to ${project.build.directory}. 088 */ 089 @Parameter(required = true) 090 private String outputDirectory; 091 092 /** 093 * A list which contains all file names, the extension is not needed. 094 */ 095 @Parameter(required = true) 096 private List<String> messageFileNames; 097 098 /** 099 * One-line descriptions for log reference categories. 100 */ 101 private static final HashMap<String, LocalizableMessage> CATEGORY_DESCRIPTIONS = new HashMap<>(); 102 static { 103 CATEGORY_DESCRIPTIONS.put("ACCESS_CONTROL", CATEGORY_ACCESS_CONTROL.get()); 104 CATEGORY_DESCRIPTIONS.put("ADMIN", CATEGORY_ADMIN.get()); 105 CATEGORY_DESCRIPTIONS.put("ADMIN_TOOL", CATEGORY_ADMIN_TOOL.get()); 106 CATEGORY_DESCRIPTIONS.put("BACKEND", CATEGORY_BACKEND.get()); 107 CATEGORY_DESCRIPTIONS.put("CONFIG", CATEGORY_CONFIG.get()); 108 CATEGORY_DESCRIPTIONS.put("CORE", CATEGORY_CORE.get()); 109 CATEGORY_DESCRIPTIONS.put("DSCONFIG", CATEGORY_DSCONFIG.get()); 110 CATEGORY_DESCRIPTIONS.put("EXTENSIONS", CATEGORY_EXTENSIONS.get()); 111 CATEGORY_DESCRIPTIONS.put("JEB", CATEGORY_JEB.get()); 112 CATEGORY_DESCRIPTIONS.put("LOG", CATEGORY_LOG.get()); 113 CATEGORY_DESCRIPTIONS.put("PLUGIN", CATEGORY_PLUGIN.get()); 114 CATEGORY_DESCRIPTIONS.put("PROTOCOL", CATEGORY_PROTOCOL.get()); 115 CATEGORY_DESCRIPTIONS.put("QUICKSETUP", CATEGORY_QUICKSETUP.get()); 116 CATEGORY_DESCRIPTIONS.put("RUNTIME_INFORMATION", CATEGORY_RUNTIME_INFORMATION.get()); 117 CATEGORY_DESCRIPTIONS.put("SCHEMA", CATEGORY_SCHEMA.get()); 118 CATEGORY_DESCRIPTIONS.put("SYNC", CATEGORY_SYNC.get()); 119 CATEGORY_DESCRIPTIONS.put("TASK", CATEGORY_TASK.get()); 120 CATEGORY_DESCRIPTIONS.put("THIRD_PARTY", CATEGORY_THIRD_PARTY.get()); 121 CATEGORY_DESCRIPTIONS.put("TOOLS", CATEGORY_TOOLS.get()); 122 CATEGORY_DESCRIPTIONS.put("USER_DEFINED", CATEGORY_USER_DEFINED.get()); 123 CATEGORY_DESCRIPTIONS.put("UTIL", CATEGORY_UTIL.get()); 124 CATEGORY_DESCRIPTIONS.put("VERSION", CATEGORY_VERSION.get()); 125 } 126 127 /** Message giving formatting rules for string keys. */ 128 public static final String KEY_FORM_MSG = ".\n\nOpenDJ message property keys must be of the form\n\n" 129 + "\t\'[CATEGORY]_[SEVERITY]_[DESCRIPTION]_[ORDINAL]\'\n\n"; 130 131 private static final String ERROR_SEVERITY_IDENTIFIER_STRING = "ERR_"; 132 133 /** FreeMarker template configuration. */ 134 private Configuration configuration; 135 136 private Configuration getConfiguration() { 137 if (configuration == null) { 138 configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); 139 configuration.setClassForTemplateLoading(GenerateSchemaDocMojo.class, "/templates"); 140 configuration.setDefaultEncoding("UTF-8"); 141 configuration.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER); 142 } 143 return configuration; 144 } 145 146 /** 147 * Writes the result of applying the FreeMarker template to the data. 148 * @param file The file to write to. 149 * @param template The name of a file in {@code resources/templates/}. 150 * @param map The data to use in the template. 151 * @throws IOException Failed to write to the file. 152 * @throws TemplateException Failed to load the template. 153 */ 154 private void writeLogRef(final File file, final String template, final Map<String, Object> map) 155 throws IOException, TemplateException { 156 // FreeMarker requires a configuration to find the template. 157 configuration = getConfiguration(); 158 159 // FreeMarker takes the data and a Writer to process the template. 160 Writer writer = null; 161 try { 162 writer = new PrintWriter(file); 163 configuration.getTemplate(template).process(map, writer); 164 } finally { 165 closeSilently(writer); 166 } 167 } 168 169 /** 170 * Represents a log reference entry for an individual message. 171 */ 172 private static class MessageRefEntry implements Comparable<MessageRefEntry> { 173 private Integer ordinal; 174 private String xmlId; 175 private String formatString; 176 177 /** 178 * Build log reference entry for an log message. 179 */ 180 public MessageRefEntry(final String msgPropKey, final Integer ordinal, final String formatString) { 181 this.formatString = formatString; 182 this.ordinal = ordinal; 183 xmlId = getXmlId(msgPropKey); 184 } 185 186 private String getXmlId(final String messagePropertyKey) { 187 // XML IDs must be unique, must begin with a letter ([A-Za-z]), 188 // and may be followed by any number of letters, digits ([0-9]), 189 // hyphens ("-"), underscores ("_"), colons (":"), and periods ("."). 190 191 final String invalidChars = "[^A-Za-z0-9\\-_:\\.]"; 192 return messagePropertyKey.replaceAll(invalidChars, "-"); 193 } 194 195 /** 196 * Returns a map of this log reference entry, suitable for use with FreeMarker. 197 * This implementation copies the message string verbatim. 198 * @return A map of this log reference entry, suitable for use with FreeMarker. 199 */ 200 public Map<String, Object> toMap() { 201 Map<String, Object> map = new HashMap<>(); 202 String id = (ordinal != null) ? ordinal.toString() : MESSAGE_NO_ORDINAL.get().toString(); 203 map.put("xmlId", "log-ref-" + xmlId); 204 map.put("id", MESSAGE_ORDINAL_ID.get(id)); 205 map.put("severity", MESSAGE_SEVERITY.get(ERROR_SEVERITY_PRINTABLE.get())); 206 map.put("message", MESSAGE_MESSAGE.get(formatString)); 207 return map; 208 } 209 210 /** 211 * Compare message entries by unique identifier. 212 * 213 * @return See {@link java.lang.Comparable#compareTo(Object)}. 214 */ 215 @Override 216 public int compareTo(MessageRefEntry mre) { 217 if (this.ordinal != null && mre.ordinal != null) { 218 return this.ordinal.compareTo(mre.ordinal); 219 } 220 return 0; 221 } 222 } 223 224 /** Represents a log reference list of messages for a category. */ 225 private static class MessageRefCategory { 226 private String category; 227 private TreeSet<MessageRefEntry> messages; 228 229 MessageRefCategory(final String category, final TreeSet<MessageRefEntry> messages) { 230 this.category = category; 231 this.messages = messages; 232 } 233 234 /** 235 * Returns a map of this log reference category, suitable for use with FreeMarker. 236 * @return A map of this log reference category, suitable for use with FreeMarker. 237 */ 238 public Map<String, Object> toMap() { 239 Map<String, Object> map = new HashMap<>(); 240 map.put("id", category); 241 map.put("category", MESSAGE_CATEGORY.get(category)); 242 List<Map<String, Object>> messageEntries = new LinkedList<>(); 243 for (MessageRefEntry entry : messages) { 244 messageEntries.add(entry.toMap()); 245 } 246 map.put("entries", messageEntries); 247 return map; 248 } 249 } 250 251 private static class MessagePropertyKey implements Comparable<MessagePropertyKey> { 252 private String description; 253 private Integer ordinal; 254 255 /** 256 * Creates a message property key from a string value. 257 * 258 * @param key 259 * from properties file 260 * @return MessagePropertyKey created from string 261 */ 262 public static MessagePropertyKey parseString(String key) { 263 int li = key.lastIndexOf("_"); 264 if (li == -1) { 265 throw new IllegalArgumentException("Incorrectly formatted key " + key); 266 } 267 268 final String description = key.substring(0, li).toUpperCase(); 269 Integer ordinal = null; 270 try { 271 String ordString = key.substring(li + 1); 272 ordinal = Integer.parseInt(ordString); 273 } catch (Exception nfe) { 274 // Ignore exception, the message has no ordinal. 275 } 276 return new MessagePropertyKey(description, ordinal); 277 } 278 279 /** 280 * Creates a parameterized instance. 281 * 282 * @param description 283 * of this key 284 * @param ordinal 285 * of this key 286 */ 287 public MessagePropertyKey(String description, Integer ordinal) { 288 this.description = description; 289 this.ordinal = ordinal; 290 } 291 292 /** 293 * Gets the ordinal of this key. 294 * 295 * @return ordinal of this key 296 */ 297 public Integer getOrdinal() { 298 return this.ordinal; 299 } 300 301 /** {@inheritDoc} */ 302 @Override 303 public String toString() { 304 if (ordinal != null) { 305 return description + "_" + ordinal; 306 } 307 return description; 308 } 309 310 /** {@inheritDoc} */ 311 @Override 312 public int compareTo(MessagePropertyKey k) { 313 if (ordinal == k.ordinal) { 314 return description.compareTo(k.description); 315 } else { 316 return ordinal.compareTo(k.ordinal); 317 } 318 } 319 } 320 321 /** 322 * For maven exec plugin execution. Generates for all included message files 323 * (sample.properties), a xml log ref file (log-ref-sample.xml) 324 * 325 * @throws MojoExecutionException 326 * if a problem occurs 327 * @throws MojoFailureException 328 * if a problem occurs 329 */ 330 @Override 331 public void execute() throws MojoExecutionException, MojoFailureException { 332 String projectBuildDir = project.getBuild().getDirectory(); 333 334 if (!outputDirectory.contains(projectBuildDir)) { 335 String errorMsg = String.format("outputDirectory parameter (%s) must be included " 336 + "in ${project.build.directory} (%s)", outputDirectory, projectBuildDir); 337 getLog().error(errorMsg); 338 throw new MojoExecutionException(errorMsg); 339 } 340 341 Map<String, Object> map = new HashMap<>(); 342 map.put("year", new SimpleDateFormat("yyyy").format(new Date())); 343 map.put("lang", locale); 344 map.put("title", LOG_REF_TITLE.get()); 345 map.put("indexterm", LOG_REF_INDEXTERM.get()); 346 map.put("intro", LOG_REF_INTRO.get()); 347 List<Map<String, Object>> categories = new LinkedList<>(); 348 for (String category : messageFileNames) { 349 File source = new File(messagesDirectory, category + ".properties"); 350 categories.add(getCategoryMap(source, category.toUpperCase())); 351 } 352 map.put("categories", categories); 353 File file = new File(outputDirectory, "log-message-reference.xml"); 354 try { 355 createOutputDirectory(); 356 writeLogRef(file, "log-message-reference.ftl", map); 357 } catch (Exception e) { 358 throw new MojoFailureException(e.getMessage(), e); 359 } 360 } 361 362 private void createOutputDirectory() throws IOException { 363 File outputDir = new File(outputDirectory); 364 if (outputDir != null && !outputDir.exists()) { 365 if (!outputDir.mkdirs()) { 366 throw new IOException("Failed to create output directory."); 367 } 368 } 369 } 370 371 private Map<String, Object> getCategoryMap(File source, String globalCategory) throws MojoExecutionException { 372 Properties properties = new Properties(); 373 try { 374 properties.load(new FileInputStream(source)); 375 Map<MessagePropertyKey, String> errorMessages = loadErrorProperties(properties); 376 TreeSet<MessageRefEntry> messageRefEntries = new TreeSet<>(); 377 Set<Integer> usedOrdinals = new HashSet<>(); 378 379 for (MessagePropertyKey msgKey : errorMessages.keySet()) { 380 String formatString = errorMessages.get(msgKey).replaceAll("<", "<"); 381 Integer ordinal = msgKey.getOrdinal(); 382 if (ordinal != null && usedOrdinals.contains(ordinal)) { 383 throw new Exception("The ordinal value \'" + ordinal + "\' in key " + msgKey 384 + " has been previously defined in " + source + KEY_FORM_MSG); 385 } 386 usedOrdinals.add(ordinal); 387 messageRefEntries.add(new MessageRefEntry(msgKey.toString(), ordinal, formatString)); 388 } 389 390 return messageRefEntries.isEmpty() 391 ? new HashMap<String, Object>() 392 : new MessageRefCategory(globalCategory, messageRefEntries).toMap(); 393 } catch (Exception e) { 394 throw new MojoExecutionException(e.getMessage(), e); 395 } 396 } 397 398 private Map<MessagePropertyKey, String> loadErrorProperties(Properties properties) throws Exception { 399 Map<MessagePropertyKey, String> errorMessage = new TreeMap<>(); 400 for (Object propO : properties.keySet()) { 401 String propKey = propO.toString(); 402 try { 403 // Document only ERROR messages. 404 if (propKey.startsWith(ERROR_SEVERITY_IDENTIFIER_STRING)) { 405 MessagePropertyKey key = MessagePropertyKey.parseString(propKey); 406 String formatString = properties.getProperty(propKey); 407 errorMessage.put(key, formatString); 408 } 409 } catch (IllegalArgumentException iae) { 410 throw new Exception("invalid property key " + propKey + ": " + iae.getMessage() + KEY_FORM_MSG, iae); 411 } 412 } 413 414 return errorMessage; 415 } 416}