/* * 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 * trunk/opends/resource/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 * trunk/opends/resource/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 2011 ForgeRock AS */ package org.opendj.buildtools.maven; import java.io.*; import java.nio.charset.Charset; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; /** * Goal which generates message source files from a one or more property files. * * @goal generatemessages * @phase generate-sources * @requiresDependencyResolution compile * @threadSafe */ public class GenerateMessagesMojo extends AbstractMojo { /** * Representation of a format specifier (for example %s). */ private class FormatSpecifier { private final String[] sa; /** * Creates a new specifier. * * @param sa * specifier components */ FormatSpecifier(final String[] sa) { this.sa = sa; } /** * Returns a java class associated with a particular formatter based on the * conversion type of the specifier. * * @return Class for representing the type of argument used as a replacement * for this specifier. */ Class getSimpleConversionClass() { Class c = null; final String sa4 = sa[4] != null ? sa[4].toLowerCase() : null; final String sa5 = sa[5] != null ? sa[5].toLowerCase() : null; if ("t".equals(sa4)) { c = Calendar.class; } else if ("b".equals(sa5)) { c = Boolean.class; } else if ("h".equals(sa5)) { c = Integer.class; } else if ("s".equals(sa5)) { c = CharSequence.class; } else if ("c".equals(sa5)) { c = Character.class; } else if ("d".equals(sa5) || "o".equals(sa5) || "x".equals(sa5) || "e".equals(sa5) || "f".equals(sa5) || "g".equals(sa5) || "a".equals(sa5)) { c = Number.class; } else if ("n".equals(sa5) || "%".equals(sa5)) { // ignore literals } return c; } /** * Indicates whether or not the specifier uses argument indexes (for example * 2$). * * @return boolean true if this specifier uses indexing */ boolean specifiesArgumentIndex() { return this.sa[0] != null; } } /** * Represents a message to be written into the messages files. */ private class MessageDescriptorDeclaration { private final MessagePropertyKey key; private final String formatString; private final List specifiers; private final List> classTypes; private String[] constructorArgs; /** * Creates a parameterized instance. * * @param key * of the message * @param formatString * of the message */ MessageDescriptorDeclaration(final MessagePropertyKey key, final String formatString) { this.key = key; this.formatString = formatString; this.specifiers = parse(formatString); this.classTypes = new ArrayList>(); for (final FormatSpecifier f : specifiers) { final Class c = f.getSimpleConversionClass(); if (c != null) { classTypes.add(c); } } } /** * {@inheritDoc} */ @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(getComment()); sb.append(indent(1)); sb.append("public static final "); sb.append(getDescriptorClassDeclaration()); sb.append(" "); sb.append(key.getMessageDescriptorName()); sb.append(" ="); sb.append(EOL); sb.append(indent(5)); sb.append("new "); sb.append(getDescriptorClassDeclaration()); sb.append("("); if (constructorArgs != null) { for (int i = 0; i < constructorArgs.length; i++) { sb.append(constructorArgs[i]); if (i < constructorArgs.length - 1) { sb.append(","); } } sb.append(", "); } sb.append("getClassLoader()"); sb.append(");"); return sb.toString(); } /** * Gets a string representing the message type class' variable information * (for example '') that is based on the type of arguments * specified by the specifiers in this message. * * @return String representing the message type class parameters */ String getClassTypeVariables() { final StringBuilder sb = new StringBuilder(); if (classTypes.size() > 0) { sb.append("<"); for (int i = 0; i < classTypes.size(); i++) { final Class c = classTypes.get(i); if (c != null) { sb.append(getShortClassName(c)); if (i < classTypes.size() - 1) { sb.append(","); } } } sb.append(">"); } return sb.toString(); } /** * Gets the javadoc comments that will appear above the messages declaration * in the messages file. * * @return String comment */ String getComment() { final StringBuilder sb = new StringBuilder(); sb.append(indent(1)).append("/**").append(EOL); // Unwrapped so that you can search through the descriptor // file for a message and not have to worry about line breaks final String ws = formatString; // wrapText(formatString, 70); final String[] sa = ws.split(EOL); for (final String s : sa) { sb.append(indent(1)).append(" * ").append(s).append(EOL); } sb.append(indent(1)).append(" */").append(EOL); return sb.toString(); } /** * Gets the name of the Java class that will be used to represent this * message's type. * * @return String representing the Java class name */ String getDescriptorClassDeclaration() { final StringBuilder sb = new StringBuilder(); if (useGenericMessageTypeClass()) { sb.append("LocalizableMessageDescriptor"); sb.append("."); sb.append(DESCRIPTOR_CLASS_BASE_NAME); sb.append("N"); } else { sb.append("LocalizableMessageDescriptor"); sb.append("."); sb.append(DESCRIPTOR_CLASS_BASE_NAME); sb.append(classTypes.size()); sb.append(getClassTypeVariables()); } return sb.toString(); } /** * Sets the arguments that will be supplied in the declaration of the * message. * * @param s * array of string arguments that will be passed in the constructor */ void setConstructorArguments(final String... s) { this.constructorArgs = s; } private void checkText(final String s) { int idx; // If there are any '%' in the given string, we got a bad format // specifier. if ((idx = s.indexOf('%')) != -1) { final char c = (idx > s.length() - 2 ? '%' : s.charAt(idx + 1)); throw new UnknownFormatConversionException(String.valueOf(c)); } } private String getShortClassName(final Class c) { String name; final String fqName = c.getName(); final int i = fqName.lastIndexOf('.'); if (i > 0) { name = fqName.substring(i + 1); } else { name = fqName; } return name; } private String indent(final int indent) { final char[] blankArray = new char[2 * indent]; Arrays.fill(blankArray, ' '); return new String(blankArray); } /** * Look for format specifiers in the format string. * * @param s * format string * @return list of format specifiers */ private List parse(final String s) { final List sl = new ArrayList(); final Matcher m = SPECIFIER_PATTERN.matcher(s); int i = 0; while (i < s.length()) { if (m.find(i)) { // Anything between the start of the string and the beginning // of the format specifier is either fixed text or contains // an invalid format string. if (m.start() != i) { // Make sure we didn't miss any invalid format specifiers checkText(s.substring(i, m.start())); // Assume previous characters were fixed text // al.add(new FixedString(s.substring(i, m.start()))); } // Expect 6 groups in regular expression final String[] sa = new String[6]; for (int j = 0; j < m.groupCount(); j++) { sa[j] = m.group(j + 1); } sl.add(new FormatSpecifier(sa)); i = m.end(); } else { // No more valid format specifiers. Check for possible invalid // format specifiers. checkText(s.substring(i)); // The rest of the string is fixed text // al.add(new FixedString(s.substring(i))); break; } } return sl; } /** * Indicates whether the generic message type class should be used. In * general this is when a format specifier is more complicated than we * support or when the number of arguments exceeeds the number of specific * message type classes (MessageType0, MessageType1 ...) that are defined. * * @return boolean indicating */ private boolean useGenericMessageTypeClass() { if (specifiers.size() > DESCRIPTOR_MAX_ARG_HANDLER) { return true; } else if (specifiers != null) { for (final FormatSpecifier s : specifiers) { if (s.specifiesArgumentIndex()) { return true; } } } return false; } } /** * The destination directory. * * @parameter * default-value="${project.build.directory}/generated-sources/messages" * @required */ private File outputDirectory; /** * The source directory. * * @parameter default-value="${basedir}/src/main/resources" * @required */ private File sourceDirectory; /** * Indicates whether or not message source files should be regenerated even if * they are already up to date. * * @parameter default-value="false" * @required */ private boolean force; /** * The list of files we want to transfer, relative to the source directory. * * @parameter * @required */ private List messageFiles; /** * The current Maven project. * * @parameter default-value="${project}" * @readonly * @required */ private MavenProject project; /** * The base name of the specific argument handling subclasses defined below. * The class names consist of the base name followed by a number indicating * the number of arguments that they handle when creating messages or the * letter "N" meaning any number of arguments. */ private static final String DESCRIPTOR_CLASS_BASE_NAME = "Arg"; /** * The maximum number of arguments that can be handled by a specific subclass. * If you define more subclasses be sure to increment this number * appropriately. */ private static final int DESCRIPTOR_MAX_ARG_HANDLER = 11; private static final String SPECIFIER_REGEX = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"; private static final Pattern SPECIFIER_PATTERN = Pattern .compile(SPECIFIER_REGEX); /** * The end-of-line character for this platform. */ private static final String EOL = System.getProperty("line.separator"); /** * The UTF-8 character set used for encoding/decoding files. */ private static final Charset UTF_8 = Charset.forName("UTF-8"); /** * {@inheritDoc} */ @Override public void execute() throws MojoExecutionException { if (!sourceDirectory.exists()) { throw new MojoExecutionException("Source directory " + sourceDirectory.getPath() + " does not exist"); } else if (!sourceDirectory.isDirectory()) { throw new MojoExecutionException("Source directory " + sourceDirectory.getPath() + " is not a directory"); } if (!outputDirectory.exists()) { if (outputDirectory.mkdirs()) { getLog().info( "Created message output directory: " + outputDirectory.getPath()); } else { throw new MojoExecutionException( "Unable to create message output directory: " + outputDirectory.getPath()); } } else if (!outputDirectory.isDirectory()) { throw new MojoExecutionException("Output directory " + outputDirectory.getPath() + " is not a directory"); } if (project != null) { getLog().info("Adding source directory: " + outputDirectory.getPath()); project.addCompileSourceRoot(outputDirectory.getAbsolutePath()); } for (final MessageFile messageFile : messageFiles) { processMessageFile(messageFile); } } /* * Returns the message Java stub file from this plugin's resources. */ private InputStream getStubFile() throws MojoExecutionException { return getClass().getResourceAsStream("Messages.java.stub"); } private void processMessageFile(final MessageFile messageFile) throws MojoExecutionException { final File sourceFile = messageFile.getSourceFile(sourceDirectory); final File outputFile = messageFile.getOutputFile(outputDirectory); // Decide whether to generate messages based on modification // times and print status messages. if (!sourceFile.exists()) { throw new MojoExecutionException("Message file " + messageFile.getName() + " does not exist"); } if (outputFile.exists()) { if (force || sourceFile.lastModified() > outputFile.lastModified()) { outputFile.delete(); getLog().info( "Regenerating " + outputFile.getName() + " from " + sourceFile.getName()); } else { getLog().info(outputFile.getName() + " is up to date"); return; } } else { final File packageDirectory = outputFile.getParentFile(); if (!packageDirectory.exists()) { if (!packageDirectory.mkdirs()) { throw new MojoExecutionException( "Unable to create message output directory: " + packageDirectory.getPath()); } } getLog().info( "Generating " + outputFile.getName() + " from " + sourceFile.getName()); } BufferedReader stubReader = null; PrintWriter outputWriter = null; try { stubReader = new BufferedReader(new InputStreamReader(getStubFile(), UTF_8)); outputWriter = new PrintWriter(outputFile, "UTF-8"); final Properties properties = new Properties(); properties.load(new FileInputStream(sourceFile)); for (String stubLine = stubReader.readLine(); stubLine != null; stubLine = stubReader .readLine()) { if (stubLine.contains("${MESSAGES}")) { final Map keyMap = new TreeMap(); for (final Object propO : properties.keySet()) { final String propKey = propO.toString(); final MessagePropertyKey key = MessagePropertyKey .parseString(propKey); final String formatString = properties.getProperty(propKey); keyMap.put(key, formatString); } int usesOfGenericDescriptor = 0; for (final MessagePropertyKey key : keyMap.keySet()) { final String formatString = keyMap.get(key); final MessageDescriptorDeclaration message = new MessageDescriptorDeclaration( key, formatString); message.setConstructorArguments("BASE", quote(key.toString())); outputWriter.println(message.toString()); outputWriter.println(); // Keep track of when we use the generic descriptor so that we can // report it later if (message.useGenericMessageTypeClass()) { usesOfGenericDescriptor++; } } getLog().debug(" LocalizableMessage Generated:" + keyMap.size()); getLog().debug( " LocalizableMessageDescriptor.ArgN:" + usesOfGenericDescriptor); } else { stubLine = stubLine.replace("${PACKAGE}", messageFile.getPackageName()); stubLine = stubLine.replace("${CLASS_NAME}", messageFile.getClassName()); stubLine = stubLine.replace("${BASE}", messageFile.getBaseName()); outputWriter.println(stubLine); } } } catch (final Exception e) { // Don't leave a malformed file laying around. Delete it so it will be // forced to be regenerated. if (outputFile.exists()) { outputFile.deleteOnExit(); } throw new MojoExecutionException( "An IO error occurred while generating the message file: " + e); } finally { if (stubReader != null) { try { stubReader.close(); } catch (final Exception e) { // Ignore. } } if (outputWriter != null) { try { outputWriter.close(); } catch (final Exception e) { // Ignore. } } } } private String quote(final String s) { return new StringBuilder().append("\"").append(s).append("\"").toString(); } }