blob: bccfa735a2071935f19dbc6bea6ad6f1dca5eb79 [file]
/*
Derby - Class org.apache.derbyBuild.MessageBuilder
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.derbyBuild;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
/**
* <p>
* This tool generates the engine's message strings (message_en.properties) as well
* the dita source for the SQLState documentation in the Derby Reference Guide.
* </p>
*/
public class MessageBuilder extends Task
{
/////////////////////////////////////////////////////////////////////////
//
// CONSTANTS
//
/////////////////////////////////////////////////////////////////////////
private static final String TAB_STOP = " ";
private static final String PROPERTIES_BOILERPLATE =
"###################################################\n" +
"#\n" +
"# Licensed to the Apache Software Foundation (ASF) under one or more\n" +
"# contributor license agreements. See the NOTICE file distributed with\n" +
"# this work for additional information regarding copyright ownership.\n" +
"# The ASF licenses this file to You under the Apache License, Version 2.0\n" +
"# (the \"License\"); you may not use this file except in compliance with\n" +
"# the License. You may obtain a copy of the License at\n" +
"#\n" +
"# http://www.apache.org/licenses/LICENSE-2.0\n" +
"#\n" +
"# Unless required by applicable law or agreed to in writing, software\n" +
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
"# See the License for the specific language governing permissions and\n" +
"# limitations under the License.\n" +
"#\n" +
"###################################################\n" +
"\n" +
"###################################################\n" +
"#\n" +
"# DO NOT EDIT THIS FILE!\n" +
"#\n" +
"# Instead, edit messages.xml. The ant MessageBuilder task takes\n" +
"# messages.xml as input and from it generates this file.\n" +
"#\n" +
"###################################################\n";
public static final String REF_GUIDE_BOILERPLATE =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<!DOCTYPE reference PUBLIC \"-//OASIS//DTD DITA Reference//EN\"\n" +
" \"../dtd/reference.dtd\">\n";
public static final String APACHE_LICENSE =
"Licensed to the Apache Software Foundation (ASF) under one or more\n" +
"contributor license agreements. See the NOTICE file distributed with\n" +
"this work for additional information regarding copyright ownership.\n" +
"The ASF licenses this file to You under the Apache License, Version 2.0\n" +
"(the \"License\"); you may not use this file except in compliance with\n" +
"the License. You may obtain a copy of the License at \n" +
"\n" +
"http://www.apache.org/licenses/LICENSE-2.0 \n" +
"\n" +
"Unless required by applicable law or agreed to in writing, software \n" +
"distributed under the License is distributed on an \"AS IS\" BASIS, \n" +
"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. \n" +
"See the License for the specific language governing permissions and \n" +
"limitations under the License.\n";
private static final String REF_GUIDE_NOTES =
"<!-- \n" +
APACHE_LICENSE +
"-->\n" +
"\n" +
"<!-- \n" +
"NOTE: this file is generated by the MessageBuilder task as part of a\n" +
"Derby build. Please do not hand-edit this file. Instead, please edit\n" +
"the corresponding text in messages.xml and/or MessageBuilder.\n" +
"-->\n";
/////////////////////////////////////////////////////////////////////////
//
// STATE
//
/////////////////////////////////////////////////////////////////////////
private String _xmlSourceFile;
private String _propertiesTargetFile;
private String _ditaTargetFile;
/////////////////////////////////////////////////////////////////////////
//
// INNER CLASSES
//
/////////////////////////////////////////////////////////////////////////
/**
* <p>
* XML-writing wrapper around a PrintWriter.
* </p>
*/
public static final class XMLWriter
{
// If this boolean is set, then all operations are NOPs.
private boolean _vacuous;
private PrintWriter _pw;
private ArrayList<String> _tagStack;
/**
* <p>
* Special constructor for making a vacuous writer which doesn't do
* anything. This allows us to write easy-to-read dita-generating code
* that is not cluttered with "if ( documented )" conditionals.
* </p>
*/
public XMLWriter()
{
_vacuous = true;
}
/**
* <p>
* Construct from a PrintWriter.
* </p>
*/
public XMLWriter(PrintWriter printWriter)
{
_vacuous = false;
_pw = printWriter;
_tagStack = new ArrayList<String>();
}
/**
* <p>
* Create a productive writer which actually flushes text to disk.
* </p>
*/
public XMLWriter( File file )
throws IOException
{
this(new PrintWriter(file));
}
public void flush() throws IOException
{
if ( _vacuous ) { return; }
_pw.flush();
}
public void close() throws IOException
{
if ( _vacuous ) { return; }
_pw.close();
}
/**
* <p>
* Indent and write an empty tag.
* </p>
*/
public void writeEmptyTag( String tag )
throws IOException
{
if ( _vacuous ) { return; }
writeEmptyTag( tag, "" );
}
/**
* <p>
* Indent and write an empty tag with attributes.
* </p>
*/
public void writeEmptyTag( String tag, String attributes )
throws IOException
{
if ( _vacuous ) { return; }
indent( );
if ( attributes.length() >0)
_pw.println( "<" + tag + " " + attributes + "/>");
else
_pw.println( "<" + tag + "/>");
}
/**
* <p>
* Indent and write an opening tag.
* </p>
*/
public void beginTag( String tag )
throws IOException
{
if ( _vacuous ) { return; }
beginTag( tag, "" );
}
/**
* <p>
* Indent and write an opening tag.
* </p>
*/
public void beginTag( String tag, String attributes )
throws IOException
{
if ( _vacuous ) { return; }
indent();
if (attributes.length() > 0)
_pw.println( "<" + tag + " " + attributes + ">");
else
_pw.println( "<" + tag + ">");
_tagStack.add( tag );
}
/**
* <p>
* Indent and write a closing tag.
* </p>
*/
public void endTag()
throws IOException
{
if ( _vacuous ) { return; }
String tag = (String) _tagStack.remove( _tagStack.size() -1 );
indent();
_pw.println( "</" + tag + ">");
}
/**
* <p>
* Indent and write a whole element
* </p>
*/
public void writeTextElement( String tag, String text )
throws IOException
{
if ( _vacuous ) { return; }
writeTextElement( tag, "", text );
}
/**
* <p>
* Indent and write a whole element
* </p>
*/
public void writeTextElement( String tag, String attributes, String text )
throws IOException
{
if ( _vacuous ) { return; }
indent();
if ( attributes.length() > 0 )
_pw.print( "<" + tag + " " + attributes + ">");
else
_pw.print( "<" + tag + ">");
_pw.print( text );
_pw.println( "</" + tag + ">");
}
/**
* <p>
* Indent based on the depth of our tag nesting level.
* </p>
*/
public void indent()
throws IOException
{
if ( _vacuous ) { return; }
int tabCount = _tagStack.size();
for ( int i = 0; i < tabCount; i++ ) { _pw.write( TAB_STOP ); }
}
/**
* <p>
* Print text.
* </p>
*/
public void println( String text )
throws IOException
{
if ( _vacuous ) { return; }
_pw.println( text );
}
}
/////////////////////////////////////////////////////////////////////////
//
// CONSTRUCTORS
//
/////////////////////////////////////////////////////////////////////////
/**
* <p>
* Let Ant conjure us out of thin air.
* </p>
*/
public MessageBuilder()
{}
/////////////////////////////////////////////////////////////////////////
//
// Task BEHAVIOR
//
/////////////////////////////////////////////////////////////////////////
/** <p>Let Ant set the input file name.</p>*/
public void setXmlSourceFile( String fileName ) { _xmlSourceFile = fileName;}
/** <p>Let Ant set the file name for the message property file we will write.</p>*/
public void setPropertiesTargetFile( String fileName ) { _propertiesTargetFile = fileName;}
/** <p>Let Ant set the file name for the SQLState dita file we will write.</p>*/
public void setDitaTargetFile( String fileName ) { _ditaTargetFile = fileName;}
/**
* <p>
* Read the xml message descriptors and output messages_en.properties
* and the dita source for the SQLState table in the Derby Reference Guide.
* After setting up arguments using the above setter methods, Ant
* calls this method in order to run this custom task.
* </p>
*/
public void execute()
throws BuildException
{
File source = new File( _xmlSourceFile );
File targetProperties = new File( _propertiesTargetFile );
File targetDita = new File( _ditaTargetFile );
FileWriter propertiesFW = null;
PrintWriter propertiesPW = null;
XMLWriter ditaWriter = null;
try {
propertiesFW = new FileWriter( targetProperties );
propertiesPW = new PrintWriter( propertiesFW );
ditaWriter = new XMLWriter( targetDita );
processMessages( source, propertiesPW, ditaWriter );
}
catch (Exception e)
{
throw new BuildException( "Could not generate English properties from message descriptors: " + e.getMessage(), e );
}
finally
{
try {
finishWriting( propertiesFW, propertiesPW );
if ( ditaWriter != null )
{
ditaWriter.flush();
ditaWriter.close();
}
}
catch (Exception ex)
{
throw new BuildException( "Error closing file writers.", ex );
}
}
}
/////////////////////////////////////////////////////////////////////////
//
// MINIONS TO PROCESS MESSAGE DESCRIPTORS
//
/////////////////////////////////////////////////////////////////////////
/**
* <p>
* Loop through descriptors and write appropriate output to the properties
* and dita files.
* </p>
*/
private void processMessages( File input, PrintWriter propertiesPW, XMLWriter ditaWriter )
throws Exception
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse( input );
Element root = doc.getDocumentElement(); // framing "messages" element
NodeList sections = root.getElementsByTagName( "section" );
propertiesPW.println( PROPERTIES_BOILERPLATE );
ditaWriter.println( REF_GUIDE_BOILERPLATE );
ditaWriter.beginTag( "reference", "id=\"rrefexcept71493\" xml:lang=\"en-us\"" );
{
ditaWriter.println( REF_GUIDE_NOTES );
ditaWriter.writeTextElement( "title", "SQL error messages and exceptions" );
ditaWriter.beginTag( "refbody" );
{
ditaWriter.beginTag( "section" );
{
ditaWriter.writeTextElement
(
"p",
"The following tables list <i>SQLStates</i> for exceptions. Exceptions " +
"that begin with an <i>X</i> are specific to <ph conref=\"../conrefs.dita#prod/productshortname\"></ph>."
);
}
ditaWriter.endTag();
ditaWriter.beginTag( "section" );
{
processSections( propertiesPW, ditaWriter, sections );
}
ditaWriter.endTag();
}
ditaWriter.endTag();
}
ditaWriter.endTag();
}
/**
* <p>
* Loop through sections in the message descriptor file..
* </p>
*/
private void processSections( PrintWriter propertiesPW, XMLWriter ditaWriter, NodeList nodes )
throws Exception
{
int nodeCount = nodes.getLength();
for ( int i = 0; i < nodeCount; i++ )
{
Element node = (Element) nodes.item( i );
processSection( propertiesPW, ditaWriter, node );
}
}
/**
* <p>
* Read a section from the message descriptor file.
* </p>
*/
private void processSection( PrintWriter propertiesPW, XMLWriter ditaWriter, Element section )
throws Exception
{
boolean documented = ( getFirstChild( section, "documented" ) != null );
NodeList families = section.getElementsByTagName( "family" );
int familyCount = families.getLength();
//
// If we don't need to document this section, then we use a vacuous XMLWriter
// which NOPs all writes.
//
if ( !documented ) { ditaWriter = new XMLWriter(); }
for ( int i = 0; i < familyCount; i++ )
{
Element family = (Element) families.item( i );
processFamily( propertiesPW, ditaWriter, family );
}
}
/**
* <p>
* Read a family of message descriptors
* </p>
*/
private void processFamily( PrintWriter propertiesPW, XMLWriter ditaWriter, Element family )
throws Exception
{
String title = squeezeText( getFirstChild( family, "title" ) );
NodeList messages = family.getElementsByTagName( "msg" );
int messageCount = messages.getLength();
ditaWriter.beginTag( "table" );
{
ditaWriter.writeTextElement( "title", title );
ditaWriter.writeTextElement( "desc", "This table lists the SQLStates and message text for the " + title + " exceptions." );
ditaWriter.beginTag( "tgroup", "cols=\"2\"" );
{
ditaWriter.writeEmptyTag( "colspec", "colname=\"col1\" colnum=\"1\" colwidth=\"1*\"" );
ditaWriter.writeEmptyTag( "colspec", "colname=\"col2\" colnum=\"2\" colwidth=\"7.5*\"" );
ditaWriter.beginTag( "thead" );
{
ditaWriter.beginTag( "row", "valign=\"bottom\"" );
{
ditaWriter.writeTextElement( "entry", "colname=\"col1\"", "SQLSTATE" );
ditaWriter.writeTextElement( "entry", "colname=\"col2\"", "Message Text" );
}
ditaWriter.endTag();
}
ditaWriter.endTag();
ditaWriter.beginTag( "tbody" );
{
for ( int i = 0; i < messageCount; i++ )
{
Element message = (Element) messages.item( i );
processMessage( propertiesPW, ditaWriter, message );
}
}
ditaWriter.endTag();
}
ditaWriter.endTag();
}
ditaWriter.endTag();
}
/**
* <p>
* Read and process a message.
* </p>
*/
private void processMessage( PrintWriter propertiesPW, XMLWriter ditaWriter, Element message )
throws Exception
{
String name = squeezeText( getFirstChild( message, "name" ) );
String sqlstate = getSQLState( name );
String rawText = squeezeText( getFirstChild( message, "text" ) );
String propertyText = escapePropertiesText( rawText );
int parameterCount = countParameters( rawText );
String[] comments = getOptionalSubElements( message, "comment" );
String[] args = getOptionalSubElements( message, "arg" );
if ( parameterCount != args.length )
{
throw new Exception( name + " has " + parameterCount + " parameters but " + args.length + " nested args." );
}
String displayText;
if (rawText.indexOf('\'')>=0)
{
displayText = replaceSpecialChars( escapeTextWithAQuote( rawText ) );
displayText = plugInArgs( displayText , args );
}
else
{
displayText = plugInArgs( replaceSpecialChars( rawText), args ) ;
}
ditaWriter.beginTag( "row" );
{
ditaWriter.writeTextElement( "entry", "colname=\"col1\"", sqlstate );
ditaWriter.writeTextElement( "entry", "colname=\"col2\"", displayText );
}
ditaWriter.endTag();
propertiesPW.println( "" );
if ( comments.length != 0 )
{
for ( int i = 0; i < comments.length; i++ )
{
propertiesPW.println( "# " + comments[ i ] );
}
propertiesPW.println( "#" );
}
if ( args.length != 0 )
{
propertiesPW.println( "# Arguments:" );
propertiesPW.println( "#" );
for ( int i = 0; i < args.length; i++ )
{
propertiesPW.println( "# {" + i + "} = " + args[ i ] );
}
propertiesPW.println( "#" );
}
propertiesPW.println( name + "=" + propertyText );
}
/**
* <p>
* Convert a message handle into a SQLState, stripping off trailing
* encodings as necessary.
* </p>
*/
private String getSQLState( String name )
{
if ( name.length() <= 5 ) { return name; }
else { return name.substring( 0, 5 ); }
}
/**
* <p>
* Get some optional sub-elements.
* </p>
*/
private String[] getOptionalSubElements( Element message, String subElementTag )
throws Exception
{
NodeList options = message.getElementsByTagName( subElementTag );
int count = options.getLength();
String[] retval = new String[ count ];
for ( int i = 0; i < count; i++ )
{
Element option = (Element) options.item( i );
retval[ i ] = squeezeText( option );
}
return retval;
}
/**
* <p>
* Count the substitutable arguments in an internationalized message string.
* These arguments have the form {n} where n is a number.
* </p>
*/
private int countParameters( String text )
{
int argCount = 0;
int argIdx = 0;
while( true )
{
argIdx = text.indexOf( '{', argIdx );
if ( argIdx >= 0 )
{
argCount++;
argIdx++;
}
else { break; }
}
return argCount;
}
/**
* <p>
* Plug arg values into parameter slots in an internationalizable message
* string.
* </p>
*/
private String plugInArgs( String message, String[] rawArgs )
{
int count = rawArgs.length;
String[] cookedArgs = new String[ count ];
MessageFormat format = new MessageFormat(message);
// add xml angle brackets around the args
for ( int i = 0; i < count; i++ )
{
cookedArgs[ i ] = "<varname>&lt;" + rawArgs[ i ] + "&gt;</varname>";
format.setFormatByArgumentIndex(i, null); // use plain string format
}
return format.format(cookedArgs);
}
/////////////////////////////////////////////////////////////////////////
//
// GENERALLY USEFUL MINIONS
//
/////////////////////////////////////////////////////////////////////////
/**
* <p>
* Echo a message to the console.
* </p>
*/
private void echo( String text )
{
log( text, Project.MSG_WARN );
}
/**
* <p>
* Flush and close file writers.
* </p>
*/
private void finishWriting( FileWriter fw, PrintWriter pw )
throws IOException
{
if ( (fw == null) || (pw == null) ) { return; }
pw.flush();
fw.flush();
pw.close();
fw.close();
}
////////////////////////////////////////////////////////
//
// XML MINIONS
//
////////////////////////////////////////////////////////
public static Element getFirstChild( Element node, String childName )
throws Exception
{
return (Element) node.getElementsByTagName( childName ).item( 0 );
}
/**
* <p>
* Squeeze the text out of an Element.
* </p>
*/
public static String squeezeText( Element node )
throws Exception
{
Node textChild = node.getFirstChild();
String text = textChild.getNodeValue();
return text;
}
/**
* Replace a substring with some equivalent. For example, we would
* like to replace "&lt;" with "&amp;lt;" in the error messages.
* Add any substrings you would like to replace in the code below.
* Be aware that the first parameter to the replaceAll() method is
* interpreted as a regular expression.
*
* @param input
* A String that may contain substrings that we want to replace
* @return
* Output String where substrings selected for replacement have been
* replaced.
* @see java.util.regex.Pattern
*/
private static String replaceSpecialChars(java.lang.String input) {
String output = input.replaceAll("<", "&lt;");
output = output.replaceAll(">", "&gt;");
return output;
}
/**
* <p>
* Replace newlines with the escape sequence needed by properties files.
* Also, replace single quotes with two single quotes.
* </p>
*/
private static String escapePropertiesText( java.lang.String input )
{
String output = input.replaceAll( "\n", "\\\\n" );
output = output.replaceAll( "\'", "\'\'" );
return output;
}
/**
* <p>
* Replace single quotes with two single quotes.
* Only needed when there are parameters with quotes.
* </p>
*/
private static String escapeTextWithAQuote( java.lang.String input )
{
String output = input.replaceAll( "\'", "\'\'" );
return output;
}
}