blob: ad0f6e9acc068c0d3ccac05c294855c86309e97a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*
*/
package org.apache.qpid.server.logging;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import java.io.File;
import java.io.FileWriter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.TreeSet;
import java.util.Set;
import java.util.ResourceBundle;
public class GenerateLogMessages
{
private boolean DEBUG = false;
private String _tmplDir;
private String _outputDir;
private List<String> _logMessages = new LinkedList<String>();
private String _packageSource;
public static void main(String[] args)
{
GenerateLogMessages generator = null;
try
{
generator = new GenerateLogMessages(args);
}
catch (IllegalAccessException iae)
{
// This occurs when args does not contain Template and output dirs.
System.exit(-1);
}
catch (Exception e)
{
//This is thrown by the Velocity Engine initialisation
e.printStackTrace();
System.exit(-1);
}
try
{
System.out.println("Running LogMessage Generator");
generator.run();
}
catch (InvalidTypeException e)
{
// This occurs when a type other than 'number' appears in the paramater config {0, number...}.
System.err.println(e.getMessage());
System.exit(-1);
}
catch (Exception e)
{
e.printStackTrace();
System.exit(-1);
}
}
GenerateLogMessages(String[] args) throws Exception
{
processArgs(args);
// We need the template and input files to run.
if (_tmplDir == null || _outputDir == null || _logMessages.size() == 0 || _packageSource == null)
{
showUsage();
throw new IllegalAccessException();
}
// Initialise the Velocity Engine, Telling it where our macro lives
Properties props = new Properties();
props.setProperty("file.resource.loader.path", _tmplDir);
Velocity.init(props);
}
private void showUsage()
{
System.out.println("Broker LogMessageGenerator v.2.0");
System.out.println("Usage: GenerateLogMessages: [-d] -t <Template Dir> -o <Output Root> -s <Source Directory> <List of _logmessage.property files to process>");
System.out.println(" where -d : Additional debug loggin.\n" +
" Template Dir: Is the base template to use.");
System.out.println(" Output Root: The root form where the LogMessage Classes will be placed.");
System.out.println(" Source Dir : The root form where the logmessasge.property files can be found.");
System.out.println(" _logmessage.property files must include the full package structure.");
}
public void run() throws InvalidTypeException, Exception
{
for (String file : _logMessages)
{
debug("Processing File:" + file);
createMessageClass(file);
}
}
/**
* Process the args for:
* -t|T value for the template location
* -o|O value for the output directory
*
* @param args the commandline arguments
*/
private void processArgs(String[] args)
{
// Crude but simple...
for (int i = 0; i < args.length; i++)
{
String arg = args[i];
if (args[i].endsWith("_logmessages.properties"))
{
_logMessages.add(args[i]);
}
else if (arg.charAt(0) == '-')
{
switch (arg.charAt(1))
{
case 'o':
case 'O':
if (++i < args.length)
{
_outputDir = args[i];
}
break;
case 't':
case 'T':
if (++i < args.length)
{
_tmplDir = args[i];
}
break;
case 's':
case 'S':
if (++i < args.length)
{
_packageSource = args[i];
}
break;
case 'd':
case 'D':
DEBUG=true;
break;
}
}
}
}
/**
* This is the main method that generates the _Messages.java file.
* The class is generated by extracting the list of messges from the
* available LogMessages Resource.
*
* The extraction is done based on typeIdentifier which is a 3-digit prefix
* on the messages e.g. BRK for Broker.
*
* @throws InvalidTypeException when an unknown parameter type is used in the properties file
* @throws Exception thrown by velocity if there is an error
*/
private void createMessageClass(String file)
throws InvalidTypeException, Exception
{
VelocityContext context = new VelocityContext();
String bundle = file.replace(File.separator, ".");
int packageStartIndex = bundle.indexOf(_packageSource) + _packageSource.length() + 2;
bundle = bundle.substring(packageStartIndex, bundle.indexOf(".properties"));
System.out.println("Creating Class for bundle:" + bundle);
ResourceBundle fileBundle = ResourceBundle.getBundle(bundle, Locale.US);
// Pull the bit from /os/path/<className>.logMessages from the bundle name
String className = file.substring(file.lastIndexOf(File.separator) + 1, file.lastIndexOf("_"));
debug("Creating ClassName form file:" + className);
String packageString = bundle.substring(0, bundle.indexOf(className));
String packagePath = packageString.replace(".", File.separator);
debug("Package path:" + packagePath);
File outputDirectory = new File(_outputDir + File.separator + packagePath);
if (!outputDirectory.exists())
{
if (!outputDirectory.mkdirs())
{
throw new IllegalAccessException("Unable to create package structure:" + outputDirectory);
}
}
// Get the Data for this class and typeIdentifier
HashMap<String, Object> typeData = prepareType(className, fileBundle);
context.put("package", packageString.substring(0, packageString.lastIndexOf('.')));
//Store the resource Bundle name for the macro
context.put("resource", bundle);
// Store this data in the context for the macro to access
context.put("type", typeData);
// Create the file writer to put the finished file in
String outputFile = _outputDir + File.separator + packagePath + className + "Messages.java";
debug("Creating Java file:" + outputFile);
FileWriter output = new FileWriter(outputFile);
// Run Velocity to create the output file.
// Fix the default file encoding to 'ISO-8859-1' rather than let
// Velocity fix it. This is the encoding format for the macro.
Velocity.mergeTemplate("LogMessages.vm", "ISO-8859-1", context, output);
//Close our file.
output.flush();
output.close();
}
/**
* This method does the processing and preparation of the data to be added
* to the Velocity context so that the macro can access and process the data
*
* The given messageKey (e.g. 'BRK') uses a 3-digit code used to match
* the property key from the loaded 'LogMessages' ResourceBundle.
*
* This gives a list of messages which are to be associated with the given
* messageName (e.g. 'Broker')
*
* Each of the messages in the list are then processed to identify how many
* parameters the MessageFormat call is expecting. These parameters are
* identified by braces ('{}') so a quick count of these can then be used
* to create a defined parameter list.
*
* Rather than defaulting all parameters to String a check is performed to
* see if a 'number' value has been requested. e.g. {0. number}
* {@see MessageFormat}. If a parameter has a 'number' type then the
* parameter will be defined as an Number value. This allows for better
* type checking during compilation whilst allowing the log message to
* maintain formatting controls.
*
* OPTIONS
*
* The returned hashMap contains the following structured data:
*
* - name - ClassName ('Broker')
* - list[logEntryData] - methodName ('BRK_1001')
* - name ('BRK-1001')
* - format ('Startup : Version: {0} Build: {1}')
* - parameters (list)
* - type ('String'|'Number')
* - name ('param1')
* - options (list)
* - name ('opt1')
* - value ('AutoDelete')
*
* @param messsageName the name to give the Class e.g. 'Broker'
*
* @return A HashMap with data for the macro
*
* @throws InvalidTypeException when an unknown parameter type is used in the properties file
*/
private HashMap<String, Object> prepareType(String messsageName, ResourceBundle messages) throws InvalidTypeException
{
// Load the LogMessages Resource Bundle
Set<String> messageKeys = new TreeSet<>(messages.keySet());
//Create the return map
HashMap<String, Object> messageTypeData = new HashMap<String, Object>();
// Store the name to give to this Class <name>Messages.java
messageTypeData.put("name", messsageName);
// Prepare the list of log messages
List<HashMap> logMessageList = new LinkedList<HashMap>();
messageTypeData.put("list", logMessageList);
//Process each of the properties
for(String message : messageKeys)
{
HashMap<String, Object> logEntryData = new HashMap<String, Object>();
// Process the log message if it matches the specified key e.g.'BRK_'
if (!message.equals("package"))
{
// Method names can't have a '-' in them so lets make it '_'
// e.g. RECOVERY-STARTUP -> RECOVERY_STARTUP
logEntryData.put("methodName", message.replace('-', '_'));
// Store the real name so we can use that in the actual log.
logEntryData.put("name", message);
//Retrieve the actual log message string.
String logMessage = messages.getString(message);
// Store the value of the property so we can add it to the
// Javadoc of the method.
logEntryData.put("format", logMessage);
// Add the parameters for this message
logEntryData.put("parameters", extractParameters(logMessage));
//Add the options for this messages
logEntryData.put("options", extractOptions(logMessage));
//Add this entry to the list for this class
logMessageList.add(logEntryData);
}
}
return messageTypeData;
}
/**
* This method is responsible for extracting the options form the given log
* message and providing a HashMap with the expected data:
* - options (list)
* - name ('opt1')
* - value ('AutoDelete')
*
* The options list that this method provides must contain HashMaps that
* have two entries. A 'name' and a 'value' these strings are important as
* they are used in LogMessage.vm to extract the stored String during
* processing of the template.
*
* The 'name' is a unique string that is used to name the boolean parameter
* and refer to it later in the method body.
*
* The 'value' is used to provide documentation in the generated class to
* aid readability.
*
* @param logMessage the message from the properties file
*
* @return list of option data
*/
private List<HashMap<String, String>> extractOptions(String logMessage)
{
// Split the string on the start brace '{' this will give us the
// details for each parameter that this message contains.
// NOTE: Simply splitting on '[' prevents the use of nested options.
// Currently there is no demand for nested options
String[] optionString = logMessage.split("\\[");
// Taking an example of:
// 'Text {n,type,format} [option] text {m} [option with param{p}] more'
// This would give us:
// 0 - Text {n,type,format}.
// 1 - option] text {m}.
// 2 - option with param{p}] more.
// Create the parameter list for this item
List<HashMap<String, String>> options = new LinkedList<HashMap<String, String>>();
// Check that we have some parameters to process
// Skip 0 as that will not be the first entry
// Text {n,type,format} [option] text {m} [option with param{p}] more
if (optionString.length > 1)
{
for (int index = 1; index < optionString.length; index++)
{
// Use a HashMap to store the type,name of the parameter
// for easy retrieval in the macro template
HashMap<String, String> option = new HashMap<String, String>();
// Locate the end of the Option
// NOTE: it is this simple matching on the first ']' that
// prevents the nesting of options.
// Currently there is no demand for nested options
int end = optionString[index].indexOf("]");
// The parameter type
String value = optionString[index].substring(0, end);
// Simply name the parameters by index.
option.put("name", "opt" + index);
//Store the value e.g. AutoDelete
// We trim as we don't need to include any whitespace that is
// used for formating. This is only used to aid readability of
// of the generated code.
option.put("value", value.trim());
options.add(option);
}
}
return options;
}
/**
* This method is responsible for extract the parameters that are requried
* for this log message. The data is returned in a HashMap that has the
* following structure:
* - parameters (list)
* - type ('String'|'Number')
* - name ('param1')
*
* The parameters list that is provided must contain HashMaps that have the
* two named entries. A 'type' and a 'name' these strings are importans as
* they are used in LogMessage.vm to extract and the stored String during
* processing of the template
*
* The 'type' and 'name' values are used to populate the method signature.
* This is what gives us the compile time validation of log messages.
*
* The 'name' must be unique as there may be many parameters. The value is
* also used in the method body to pass the values through to the
* MessageFormat class for formating the log message.
*
* @param logMessage the message from the properties file
*
* @return list of option data
*
* @throws InvalidTypeException if the FormatType is specified and is not 'number'
*/
private List<HashMap<String, String>> extractParameters(String logMessage)
throws InvalidTypeException
{
// Split the string on the start brace '{' this will give us the
// details for each parameter that this message contains.
String[] parametersString = logMessage.split("\\{");
// Taking an example of 'Text {n[,type]} text {m} more text {p}'
// This would give us:
// 0 - Text.
// 1 - n[,type]} text.
// 2 - m} more text.
// 3 - p}.
// Create the parameter list for this item
List<HashMap<String, String>> parameters = new LinkedList<HashMap<String, String>>();
/*
Check that we have some parameters to process
Skip 0 as that will not be the first entry
Text {n[,type]} text {m} more text {p}.
*/
if (parametersString.length > 1)
{
for (int index = 1; index < parametersString.length; index++)
{
// Use a HashMap to store the type,name of the parameter
// for easy retrieval in the macro template
HashMap<String, String> parameter = new HashMap<String, String>();
/*
Check for any properties of the parameter :
e.g. {0} vs {0,number} vs {0,number,xxxx}.
*/
int typeIndex = parametersString[index].indexOf(",");
int typeIndexEnd = parametersString[index].indexOf("}", typeIndex);
// The parameter type
String type;
//Be default all types are Strings
if (typeIndex == -1 || typeIndexEnd == -1)
{
type = "String";
}
else
{
//Check string ',....}' for existence of number
// to identify this parameter as an integer
// This allows for a style value to be present
// Only check the text inside the braces '{}'
String typeString = parametersString[index].substring(typeIndex, typeIndexEnd);
if (typeString.contains("number") || typeString.contains("choice"))
{
type = "Number";
}
else
{
throw new InvalidTypeException("Invalid type(" + typeString + ") index (" + parameter.size() + ") in message:" + logMessage);
}
}
//Store the data
parameter.put("type", type);
// Simply name the parameters by index.
parameter.put("name", "param" + index);
parameters.add(parameter);
}
}
return parameters;
}
/**
* Just a inner exception to be able to identify when a type that is not
* 'number' occurs in the message parameter text.
*/
private static class InvalidTypeException extends Exception
{
public InvalidTypeException(String message)
{
super(message);
}
}
public void debug(String msg)
{
if (DEBUG)
{
System.out.println(msg);
}
}
}