/*
* 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-2012 ForgeRock AS
*/
package org.opends.build.tools;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import static org.opends.build.tools.Utilities.*;
import org.opends.messages.Category;
import org.opends.messages.Severity;
import org.opends.messages.MessageDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.DataOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Properties;
import java.util.List;
import java.util.ArrayList;
import java.util.TreeSet;
import java.util.UnknownFormatConversionException;
import java.util.Calendar;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.HashSet;
import java.util.Set;
import java.util.EnumSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Generates a Java class containing representations of messages
* found in a properties file.
*/
public class GenerateMessageFile extends Task {
private File source;
private File dest;
private boolean overwrite;
private boolean writeLogRef;
static private final String MESSAGES_FILE_STUB =
"resource/Messages.java.stub";
/*
* 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.
*/
static private String REGISTRY_FILE_NAME;
static private final String DESCRIPTORS_REG = "descriptors.reg";
/**
* Used to set a category for all messages in the property file.
* If set, the category for each message need not be encoded in
* the message's property file key.
*/
static private final String GLOBAL_CATEGORY = "global.category";
/**
* Used to set a severity for all messages in the property file.
* If set, the severity for each message need not be encoded in
* the message's property file key.
*/
static private final String GLOBAL_SEVERITY = "global.severity";
/**
* Used to set a category mask for all messages in the property
* file. If set, the category will automatically be assigned
* USER_DEFINED and the value of GLOBAL_CATEGORY
* will be ignored.
*/
static private final String GLOBAL_CATEGORY_MASK = "global.mask";
/**
* When true generates messages that have no ordinals.
*/
static private final String GLOBAL_ORDINAL = "global.ordinal";
/**
* When true and if the Java Web Start property is set use the class loader of
* the jar where the MessageDescriptor is contained to retrieve the
* ResourceBundle.
*/
static private final String GLOBAL_USE_MESSAGE_JAR_IF_WEBSTART =
"global.use.message.jar.if.webstart";
static private final Set DIRECTIVE_PROPERTIES = new HashSet();
static {
DIRECTIVE_PROPERTIES.add(GLOBAL_CATEGORY);
DIRECTIVE_PROPERTIES.add(GLOBAL_CATEGORY_MASK);
DIRECTIVE_PROPERTIES.add(GLOBAL_SEVERITY);
DIRECTIVE_PROPERTIES.add(GLOBAL_ORDINAL);
DIRECTIVE_PROPERTIES.add(GLOBAL_USE_MESSAGE_JAR_IF_WEBSTART);
}
static private final String SPECIFIER_REGEX =
"%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
private final Pattern SPECIFIER_PATTERN = Pattern.compile(SPECIFIER_REGEX);
/**
* Message giving formatting rules for string keys.
*/
static public String KEY_FORM_MSG;
static {
KEY_FORM_MSG = new StringBuilder()
.append(".\n\nOpenDJ message property keys must be of the form\n\n")
.append("\t\'[CATEGORY]_[SEVERITY]_[DESCRIPTION]_[ORDINAL]\'\n\n")
.append("where\n\n")
.append("CATEGORY is one of ")
.append(EnumSet.allOf(Category.class))
.append("\n\nSEVERITY is one of ")
.append(Severity.getPropertyKeyFormSet().toString())
.append("\n\nDESCRIPTION is a descriptive string composed ")
.append("of uppercase character, digits and underscores ")
.append("describing the purpose of the message ")
.append("\n\nORDINAL is an integer between 0 and 65535 that is ")
.append("unique to other messages defined in this file.\n\n")
.append("You can relax the mandate for including the CATEGORY, ")
.append("SEVERITY, and/or ORDINAL by including one or more ")
.append("of the following respective property directives in your ")
.append("properties file: ")
.append(GLOBAL_CATEGORY)
.append(", ")
.append(GLOBAL_SEVERITY)
.append(", ")
.append(GLOBAL_ORDINAL)
.append("and setting their value appropriately.")
.toString();
}
/*
* ISO_LANGUAGES contains all official supported languages for i18n
*/
private static final List ISO_LANGUAGES =
Arrays.asList(Locale.getISOLanguages());
/*
* ISO_COUNTRIES contains all official supported countries for i18n
*/
private static final List ISO_COUNTRIES =
Arrays.asList(Locale.getISOCountries());
/*
* A Pattern instance that matches "__.properties"
* where can be anything including '_'
* a two characters code contained in the ISO_LANGUAGES list
* a two characters code contained in the ISO_COUNTRIES list
*/
private static final Pattern LANGUAGE_COUNTRY_MATCHER =
Pattern.compile("(.*)_([a-z]{2})_([A-Z]{2}).properties");
/*
* A Pattern instance that matches "_.properties"
* where and have same definition as above.
*/
private static final Pattern LANGUAGE_MATCHER =
Pattern.compile("(.*)_([a-z]{2}).properties");
/**
* Representation of a format specifier (for example %s).
*/
private class FormatSpecifier {
private String[] sa;
/**
* Creates a new specifier.
* @param sa specifier components
*/
FormatSpecifier(String[] sa) {
this.sa = sa;
}
/**
* Indicates whether or not the specifier uses argument
* indexes (for example 2$).
* @return boolean true if this specifier uses indexing
*/
public boolean specifiesArgumentIndex() {
return this.sa[0] != null;
}
/**
* 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.
*/
public Class> getSimpleConversionClass() {
Class> c = null;
String sa4 = sa[4] != null ? sa[4].toLowerCase() : null;
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;
}
}
/**
* Represents a message to be written into the messages files.
*/
private class MessageDescriptorDeclaration {
private MessagePropertyKey key;
private String formatString;
private List specifiers;
private List> classTypes;
private String[] constructorArgs;
/**
* Creates a parameterized instance.
* @param key of the message
* @param formatString of the message
*/
public MessageDescriptorDeclaration(MessagePropertyKey key,
String formatString) {
this.key = key;
this.formatString = formatString;
this.specifiers = parse(formatString);
this.classTypes = new ArrayList>();
for (FormatSpecifier f : specifiers) {
Class> c = f.getSimpleConversionClass();
if (c != null) {
classTypes.add(c);
}
}
}
/**
* Gets the name of the Java class that will be used to represent
* this message's type.
* @return String representing the Java class name
*/
public String getDescriptorClassDeclaration() {
StringBuilder sb = new StringBuilder();
if (useGenericMessageTypeClass()) {
sb.append(getShortClassName(MessageDescriptor.class));
sb.append(".");
sb.append(MessageDescriptor.DESCRIPTOR_CLASS_BASE_NAME);
sb.append("N");
} else {
sb.append(getShortClassName(MessageDescriptor.class));
sb.append(".");
sb.append(MessageDescriptor.DESCRIPTOR_CLASS_BASE_NAME);
sb.append(classTypes.size());
sb.append(getClassTypeVariables());
}
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
*/
public String getClassTypeVariables() {
StringBuilder sb = new StringBuilder();
if (classTypes.size() > 0) {
sb.append("<");
for (int i = 0; i < classTypes.size(); i++) {
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
*/
public String getComment() {
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
String ws = formatString; // wrapText(formatString, 70);
String[] sa = ws.split(EOL);
for (String s : sa) {
sb.append(indent(1)).append(" * ").append(s).append(EOL);
}
sb.append(indent(1)).append(" */").append(EOL);
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
*/
public void setConstructorArguments(String... s) {
this.constructorArgs = s;
}
/**
* {@inheritDoc}
*/
public String toString() {
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();
}
/**
* 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() > MessageDescriptor.DESCRIPTOR_MAX_ARG_HANDLER) {
return true;
} else if (specifiers != null) {
for (FormatSpecifier s : specifiers) {
if (s.specifiesArgumentIndex()) {
return true;
}
}
}
return false;
}
/**
* Look for format specifiers in the format string.
* @param s format string
* @return list of format specifiers
*/
private List parse(String s) {
List sl = new ArrayList();
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
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;
}
private void checkText(String s) {
int idx;
// If there are any '%' in the given string, we got a bad format
// specifier.
if ((idx = s.indexOf('%')) != -1) {
char c = (idx > s.length() - 2 ? '%' : s.charAt(idx + 1));
throw new UnknownFormatConversionException(String.valueOf(c));
}
}
}
/**
* Sets the source of the messages.
* @param source File representing the properties
* file containing messages
*/
public void setSourceProps(File source) {
this.source = source;
}
/**
* Sets the file that will be generated containing
* declarations of messages from source
.
* @param dest File destination
*/
public void setDestJava(File dest) {
this.dest = dest;
try {
File descriptorsRegFile =
new File(dest.getParentFile(), DESCRIPTORS_REG);
if (REGISTRY_FILE_NAME != null) {
// if REGISTRY_FILE_NAME is already set, ensure that we computed the
// same one
File prevDescriptorsRegFile = new File(REGISTRY_FILE_NAME);
if (!prevDescriptorsRegFile.equals(descriptorsRegFile)) {
throw new BuildException("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 {
REGISTRY_FILE_NAME = descriptorsRegFile.getCanonicalPath();
}
} catch (Exception e) {
throw (new BuildException(e));
}
}
/**
* Indicates when true that an existing destination
* file will be overwritten.
* @param o boolean where true means overwrite
*/
public void setOverwrite(boolean o) {
this.overwrite = o;
}
/**
* Indicates when true that an XML log message reference should be generated
* instead of a Java messages file.
*
* @param xml
* true means write an XML log message reference
*/
public void setWriteLogRef(boolean xml) {
this.writeLogRef = xml;
}
/**
* Represents a log reference entry for an individual message.
*/
private class MessageRefEntry implements Comparable
{
private Severity severity;
private Integer id;
private String formatString;
/**
* Build log reference entry for an log message.
*
* @param category
* @param severity
* @param ordinal
* @param formatString
*/
public MessageRefEntry(final Category category, final Severity severity,
final Integer ordinal, final String formatString)
{
this.severity = severity;
this.formatString = formatString;
id = calculateId(category, severity, ordinal);
}
private Integer calculateId(final Category category,
final Severity severity, final Integer ordinal)
{
return new Integer(ordinal | category.getMask() | severity.getMask());
}
/**
* 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>.
*/
public String toString()
{
return
" " + EOL
+ " ID: " + id.intValue() + " " + EOL
+ " " + EOL
+ " Severity: " + severity.name() + " " + EOL
+ " Message: " + formatString + " " + EOL
+ " " + EOL + " " + EOL;
}
/**
* Calls {@link #toString()}.
*/
public String toXML()
{
return toString();
}
/**
* The unique message identifier is calculated using the category, severity,
* and message ordinal.
*
* @return unique message identifier
*/
public Integer getId()
{
return this.id;
}
/**
* Compare message entries by unique identifier.
*
* @return See {@link java.lang.Comparable#compareTo(Object)}.
*/
public int compareTo(MessageRefEntry mre)
{
return this.id.compareTo(mre.getId());
}
}
/**
* One-line descriptions for log reference categories
*/
static private HashMap CATEGORY_DESCRIPTIONS;
static {
CATEGORY_DESCRIPTIONS = new HashMap();
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.");
}
/**
* Represents a log reference list of messages for a category.
*/
private class MessageRefCategory
{
private Category category;
private TreeSet messages;
private String description;
MessageRefCategory(final Category category,
final TreeSet messages)
{
this.category = category;
this.messages = messages;
this.description = getDescription(category);
}
private String getDescription(final Category category)
{
return "This category concerns messages associated with "
+ CATEGORY_DESCRIPTIONS.get(category.name()) + " " + EOL;
}
/**
* Return a DocBook XML <variablelist> of this log reference category.
*
* @return DocBook XML <variablelist>
*/
public String toString()
{
StringBuilder entries = new StringBuilder();
for (MessageRefEntry entry : messages)
{
entries.append(entry.toXML());
}
return getVariablelistHead() + entries.toString() + 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 new StringBuilder()
.append("").append(EOL)
.append("").append(EOL)
.toString();
}
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.name() + " "
+ EOL + " " + this.description;
}
private String getVariablelistTail()
{
return " " + EOL;
}
}
/**
* {@inheritDoc}
*/
@Override
public void execute() throws BuildException {
if ( this.dest == null ) {
// this is an example-plugin call:
// - check the source file is not a localization
// - guess the destination filename from source filename
String sourcefilename = source.getAbsolutePath();
int filenameIndex = sourcefilename.lastIndexOf(File.separator)+1;
String pathname = sourcefilename.substring(0, filenameIndex);
String filename = sourcefilename.substring(filenameIndex);
/*
* Make sure only .properties are generated thus avoiding to
* generate messages for localized properties files.
*/
Matcher matcher = LANGUAGE_COUNTRY_MATCHER.matcher(filename);
if ( matcher.find() ) {
if ( ISO_LANGUAGES.contains(matcher.group(2))
&& ISO_COUNTRIES.contains(matcher.group(3)) ) {
// do not generate message for __.properties
return;
}
}
matcher = LANGUAGE_MATCHER.matcher(filename);
if ( matcher.find() ) {
if ( ISO_LANGUAGES.contains(matcher.group(2)) ) {
// do not generate message for _.properties
return;
}
}
// filename without ".properties"
filename = filename.substring(0, filename.length()-11);
// change to src-generated directory keeping package name
if (writeLogRef) {
pathname = pathname.replace(getProject().getProperty("msg.dir"),
getProject().getProperty("msg.logref.dir"));
} else {
pathname = pathname.replace(getProject().getProperty("msg.dir"),
getProject().getProperty("msg.javagen.dir"));
}
// append characters from filename to pathname starting with an uppercase
// letter, ignoring '_' and uppering all characters prefixed with "_"
StringBuilder sb = new StringBuilder(pathname);
boolean upperCaseNextChar = true;
for(char c : filename.toCharArray()) {
if ( c == '_' || c == '-' ) {
upperCaseNextChar = true;
continue;
}
if ( upperCaseNextChar ) {
sb.append(Character.toUpperCase(c));
upperCaseNextChar = false;
} else {
sb.append(c);
}
}
if (writeLogRef) {
sb.append("Messages.xml");
} else {
sb.append("Messages.java");
}
setDestJava(new File(sb.toString()));
}
BufferedReader stubReader = null;
PrintWriter destWriter = null;
try {
// Decide whether to generate messages based on modification
// times and print status messages.
if (!source.exists()) {
throw new BuildException("file " + source.getName() +
" does not exist");
}
if (dest.exists()) {
if (this.overwrite || source.lastModified() > dest.lastModified()) {
dest.delete();
log("Regenerating " + dest.getName() + " from " + source.getName());
} else {
log(dest.getName() + " is up to date");
return;
}
} else {
File javaGenDir = dest.getParentFile();
if (!javaGenDir.exists()) {
javaGenDir.mkdirs();
}
log("Generating " + dest.getName() + " from " + source.getName());
}
stubReader = new BufferedReader(new InputStreamReader(getStubFile(),
"UTF-8"));
destWriter = new PrintWriter(dest, "UTF-8");
String stubLine;
Properties properties = new Properties();
properties.load(new FileInputStream(source));
TreeSet messageRefEntries =
new TreeSet();
while (null != (stubLine = stubReader.readLine())) {
if (stubLine.contains("${MESSAGES}")) {
Integer globalOrdinal = null;
String go = properties.getProperty(GLOBAL_ORDINAL);
if (go != null) {
globalOrdinal = new Integer(go);
}
// Determine the value of the global category/mask if set
Integer globalMask = null;
Category globalCategory = null;
String gms = properties.getProperty(GLOBAL_CATEGORY_MASK);
if (gms != null) {
globalMask = Integer.parseInt(gms);
globalCategory = Category.USER_DEFINED;
} else {
String gcs = properties.getProperty(GLOBAL_CATEGORY);
if (gcs != null) {
globalCategory = Category.valueOf(gcs);
}
}
// Determine the value of the global severity
Severity globalSeverity = null;
String gss = properties.getProperty(GLOBAL_SEVERITY);
if (gss != null) {
globalSeverity = Severity.parseString(gss);
}
Map keyMap =
new TreeMap();
for (Object propO : properties.keySet()) {
String propKey = propO.toString();
try {
if (!DIRECTIVE_PROPERTIES.contains(propKey)) {
MessagePropertyKey key =
MessagePropertyKey.parseString(
propKey,
globalCategory == null,
globalSeverity == null,
globalOrdinal == null);
String formatString = properties.getProperty(propKey);
keyMap.put(key, formatString);
}
} catch (IllegalArgumentException iae) {
throw new BuildException(
"ERROR: invalid property key " + propKey +
": " + iae.getMessage() +
KEY_FORM_MSG);
}
}
int usesOfGenericDescriptor = 0;
Category firstCategory = null;
Set usedOrdinals = new HashSet();
for (MessagePropertyKey key : keyMap.keySet()) {
String formatString = keyMap.get(key);
MessageDescriptorDeclaration message =
new MessageDescriptorDeclaration(key, formatString);
Category c = (globalCategory != null ?
globalCategory : key.getCategory());
// Check that this category is the same as all the
// others in this file. Maybe this should be an error?
if (firstCategory != null) {
if (!firstCategory.equals(c)) {
log("WARNING: multiple categories defined in " + source);
}
} else {
firstCategory = c;
}
Severity s = (globalSeverity != null ?
globalSeverity : key.getSeverity());
if (c == null) {
throw new BuildException(
"No category could be assigned to message " +
key + ". The category " +
"must either be encoded in the property key or " +
"or must be set by including the property " +
GLOBAL_CATEGORY + " in the properties file" +
KEY_FORM_MSG);
}
if (s == null) {
throw new BuildException(
"No severity could be assigned to message " +
key + ". The severity " +
"must either be encoded in the property key or " +
"or must be set by including the property " +
GLOBAL_SEVERITY + " in the properties file" +
KEY_FORM_MSG);
}
if (globalOrdinal == null) {
Integer ordinal = key.getOrdinal();
if (usedOrdinals.contains(ordinal)) {
throw new BuildException(
"The ordinal value \'" + ordinal + "\' in key " +
key + " has been previously defined in " +
source + KEY_FORM_MSG);
} else {
usedOrdinals.add(ordinal);
}
}
if (writeLogRef) {
// Document only FATAL_ERROR and SEVERE_ERROR messages.
if (s.name().equalsIgnoreCase("FATAL_ERROR")
|| s.name().equalsIgnoreCase("SEVERE_ERROR")) {
MessageRefEntry entry =
new MessageRefEntry(
c,
s,
globalOrdinal != null ?
globalOrdinal :
key.getOrdinal(),
formatString.replaceAll("<", "<"));
messageRefEntries.add(entry);
}
} else {
message.setConstructorArguments(
"BASE",
quote(key.toString()),
globalMask != null ? globalMask.toString() : c.name(),
s.name(),
globalOrdinal != null ?
globalOrdinal.toString() :
key.getOrdinal().toString()
);
destWriter.println(message.toString());
destWriter.println();
}
// Keep track of when we use the generic descriptor
// so that we can report it later
if (message.useGenericMessageTypeClass()) {
usesOfGenericDescriptor++;
}
}
if (writeLogRef) {
if (messageRefEntries.isEmpty()) {
destWriter.println("");
} else {
MessageRefCategory mrc =
new MessageRefCategory(globalCategory, messageRefEntries);
destWriter.println(mrc.toXML());
}
}
log(" Message Generated:" + keyMap.size(), Project.MSG_VERBOSE);
log(" MessageDescriptor.ArgN:" + usesOfGenericDescriptor,
Project.MSG_VERBOSE);
} else if (!writeLogRef) {
stubLine = stubLine.replace("${PACKAGE}", getPackage());
stubLine = stubLine.replace("${CLASS_NAME}",
dest.getName().substring(0, dest.getName().length() -
".java".length()));
stubLine = stubLine.replace("${BASE}", getBase());
String useMessageJarIfWebstart =
properties.getProperty(GLOBAL_USE_MESSAGE_JAR_IF_WEBSTART);
if ((useMessageJarIfWebstart != null) &&
("true".equalsIgnoreCase(useMessageJarIfWebstart) ||
"on".equalsIgnoreCase(useMessageJarIfWebstart) ||
"true".equalsIgnoreCase(useMessageJarIfWebstart)))
{
useMessageJarIfWebstart = "true";
}
else
{
useMessageJarIfWebstart = "false";
}
stubLine = stubLine.replace("${USE_MESSAGE_JAR_IF_WEBSTART}",
useMessageJarIfWebstart);
destWriter.println(stubLine);
}
}
registerMessageDescriptor(getMessageDescriptorFullClassName());
stubReader.close();
destWriter.close();
} catch (Exception e) {
// Don't leave a malformed file laying around. Delete
// it so it will be forced to be regenerated.
if (dest.exists()) {
dest.deleteOnExit();
}
e.printStackTrace();
throw new BuildException("Error processing " + source +
": " + e.getMessage());
} finally {
if (stubReader != null) {
try {
stubReader.close();
} catch (Exception e){
// ignore
}
}
if (destWriter != null) {
try {
destWriter.close();
} catch (Exception e){
// ignore
}
}
}
}
private String getMessageDescriptorFullClassName() {
return getPackage() + "." + getMessageDescriptorClassName();
}
private String getMessageDescriptorClassName() {
return dest.getName().substring(
0, dest.getName().length() - ".java".length());
}
private String getBase() {
String srcPath = unixifyPath(source.getAbsolutePath());
String base = srcPath.substring(srcPath.lastIndexOf("/") + 1,
srcPath.length() - ".properties".length());
return base;
}
private String getPackage() {
if (writeLogRef) { return "dummy.package.name"; }
String destPath = unixifyPath(dest.getAbsolutePath());
String msgJavaGenDir = unixifyPath(
getProject().getProperty("msg.javagen.dir"));
String c = destPath.substring(msgJavaGenDir.length()+1);
c = c.replace('/', '.');
c = c.substring(0, c.lastIndexOf(".")); // strip .java
c = c.substring(0, c.lastIndexOf(".")); // strip class name
return c;
}
static private String indent(int indent) {
char[] blankArray = new char[2 * indent];
Arrays.fill(blankArray, ' ');
return new String(blankArray);
}
static private String quote(String s) {
return new StringBuilder()
.append("\"")
.append(s)
.append("\"")
.toString();
}
static private String getShortClassName(Class> c) {
String name;
String fqName = c.getName();
int i = fqName.lastIndexOf('.');
if (i > 0) {
name = fqName.substring(i + 1);
} else {
name = fqName;
}
return name;
}
/**
* Writes a record in the messages registry for the specifed
* class name.
* @param descClassName name of the message descriptor class
* @return true if the class was acutally added to the registry;
* false indicates that the class was already present.
* @throws IOException if there is a problem with the file I/O
*/
private boolean registerMessageDescriptor(String descClassName)
throws IOException
{
boolean classAdded = false;
File registry = getRegistryFile();
if (!isDescriptorRegistered(descClassName)) {
FileOutputStream file = new FileOutputStream(registry,true);
DataOutputStream out = new DataOutputStream(file);
out.writeBytes(descClassName);
out.writeBytes("\n");
out.flush();
out.close();
}
return classAdded;
}
private boolean isDescriptorRegistered(String descClassName)
throws IOException
{
boolean isRegistered = false;
BufferedReader reader = new BufferedReader(
new FileReader(getRegistryFile()));
String line;
while(null != (line = reader.readLine())) {
if (line.trim().equals(descClassName.trim())) {
isRegistered = true;
break;
}
}
return isRegistered;
}
private File getRegistryFile() throws IOException {
File registry = new File(REGISTRY_FILE_NAME);
if (!registry.exists()) {
File parent = registry.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
registry.createNewFile();
}
return registry;
}
private File getProjectBase() {
return getProject().getBaseDir();
}
private String unixifyPath(String path) {
return path.replace("\\", "/");
}
/*
* Returns the stub file ("resource/Messages.java.stub") from the appropriate
* location: ant or jar file.
*/
private InputStream getStubFile() {
InputStream result = null;
File stub = new File(getProjectBase(), MESSAGES_FILE_STUB);
if ( stub.exists() ) {
// this is the OpenDS's ant project calling
// Stub is located at OPENDS_ROOT/resource/Messages.java.stub
try {
result = new FileInputStream(stub);
} catch (FileNotFoundException e) {
// should neven happen
throw new BuildException("Unable to load template " +
MESSAGES_FILE_STUB + ": " + e.getMessage());
}
} else {
// this is the example plugin's ant project calling
// Stub is located at build-tools.jar:resource/Messages.java.stub
result = getClass().getResourceAsStream(MESSAGES_FILE_STUB);
}
return result;
}
/**
* For testing.
* @param args from command line
*/
public static void main(String[] args) {
File source = new File("src/messages/messages/tools.properties");
File dest = new File("/tmp/org/opends/XXX.java");
GenerateMessageFile gmf = new GenerateMessageFile();
if (args.length > 0 && args[0].equalsIgnoreCase("generateMessageReference"))
{
dest = new File("/tmp/tools-ref.xml");
gmf.setWriteLogRef(true);
}
gmf.setOverwrite(true);
gmf.setDestJava(dest);
gmf.setSourceProps(source);
gmf.execute();
}
}