/* Copyright Tom Valine 2002,2014 All Rights Reserved. ****************************************************************/
package com.kre8orz.i18n.processor;
import com.kre8orz.i18n.annotation.*;
import com.kre8orz.i18n.processor.I18NVisitor.MsgInfo;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.JavaFileManager.Location;
import javax.tools.StandardLocation;
import static com.kre8orz.i18n.processor.I18NProcessorConstants.*;
import static com.kre8orz.i18n.processor.I18NProcessorMessages.*;
import static com.kre8orz.i18n.processor.I18NProcessorTemplates.*;
/**
* The annotation processor used to generate the resource bundles and catalog class from annotated message interfaces.
*
* <p>The following properties are available for processor customization. How these properties are passed to your
* compiler are vendor specific, however for Oracle Java compilers 1.7.x or higher, they are specified using <tt><span
* style="white-space:nowrap;">-A<property_name>[=<property_value>]</span></tt></p>
*
* <ul>
* <li>com.kre8orz.i18n.processor.I18NProcessor.catalogClass - Specifies the fully qualified catalog class name. If
* the word <b>null</b> is specified then the catalog class is not generated.</li>
* <li>com.kre8orz.i18n.processor.I18NProcessor.obfuscate - Specifies the key phrase to use for obfuscation. If this
* option is supplied then the generated bundle messages will be obfuscated. If this option is left empty or is
* omitted then no obfuscation is performed.</li>
* <li>
* <p>com.kre8orz.i18n.processor.I18NProcessor.language - Specifies the logging/message output language. Currently
* supported languages are:</p>
*
* <ul>
* <li>English (en_US) This is the default.</li>
* <li>Spanish (es_ES)</li>
* </ul>
* </li>
* </ul>
*
* @author Tom Valine (thomas.valine@gmail.com)
*/
public final class I18NProcessor implements Processor {
//~ Static fields/initializers *************************************************************************************
/* This is a helper collection that is used to store the valid annotation
* types supported by the processor. */
private static final Set<String> _TYPES;
/* Instantiate and populate the type collection. */
static {
_TYPES = new HashSet<String>();
_TYPES.add(I18NMessage.class.getName());
_TYPES.add(I18NMessages.class.getName());
_TYPES.add(I18NResourceBundle.class.getName());
}
//~ Instance fields ************************************************************************************************
/* Helper class used to encapsulate messages generated by the processor. */
private I18NProcessorMessages _i18n;
/* The option processor and repository. */
private I18NProcessorOptions _options;
/* The processing environment to be used during processing. This is set
* by the calling code via the init(ProcessingEnvironment) method. The field should be null at any time before that
* and not null any time after
* that.*/
private ProcessingEnvironment _pe;
/* The collection of file writers for the resource bundle property files.
* This field should be null before init(ProcessingEnvironment) is called and should be initialized to an empty map
* of bundle names to record writers. The map is populated only after processing is completed and the annotation
* information has been collected. New writers are added on an as needed basis during bundle file generation.
* Existing writers are obtained from this collection and re-used when a new key-value pair needs to be written to
* an existing (yet incomplete) bundle file. At no time after writers are created and added to this collection
* should they be closed unless processing and file generation is complete and the
* processor is exiting.*/
private Map<String, PrintWriter> _resWrtr;
/* The element visitor that analyzes annotated elements sent to the
* processor by the calling code. This is the class that is responsible for collecting the associated annotation
* information that is uses to write the bundle files as well as the catalog class file. This field should be null
* until init(ProcessingEnvironment) is called at which point it is
* populated with a new instance of the class. */
private I18NVisitor _visitor;
//~ Methods ********************************************************************************************************
/**
* Returns an empty list of completions. This processor does not support this feature.
*
* @param element The element being annotated.
* @param annotation The (perhaps partial) annotation being applied to the element.
* @param member The annotation member to return possible completions for.
* @param userText Source code text to be completed.
*
* @return An empty list of completions.
*
* @see javax.annotation.processing.Processor
*/
@Override
public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation,
ExecutableElement member, String userText) {
return Collections.unmodifiableList(new ArrayList<Completion>());
}
/**
* Returns a list of supported annotations. This processor supports all annotations within the <tt>
* com.kre8orz.i18n.annotation</tt> package.
*
* @return A list of supported annotations.
*
* @see javax.annotation.processing.Processor
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.unmodifiableSet(_TYPES);
}
/**
* Returns the list of supported options. This processor supports the following options:
*
* <ul>
* <li>com.kre8orz.i18n.processor.I18NProcessor.catalogClass - Specifies the fully qualified catalog class name.
* If not specified the catalog class will be named I18NCatalog and reside in the default package.</li>
* <li>com.kre8orz.i18n.processor.I18NProcessor.obfuscate - Specifies the key phrase to use for obfuscation. If
* this option is supplied then the generated bundle messages will be obfuscated. If this option is left empty
* or is omitted then no obfuscation is performed.</li>
* <li>com.kre8orz.i18n.processor.I18NProcessor.language - Specifies the logging/message output language.
* Currently supported languages are:<br/>
*
* <ul>
* <li>English (en_US) This is the default.</li>
* <li>Spanish (es_ES)</li>
* </ul>
* </li>
* </ul>
*
* @return The list of supported options.
*
* @see javax.annotation.processing.Processor
*/
@Override
public Set<String> getSupportedOptions() {
return I18NProcessorOptions.getSupportedOptions();
}
/**
* Returns the supported source version. This processor supports Java 1.6 or greater.
*
* @return The supported source version.
*
* @see javax.annotation.processing.Processor
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
/**
* Initializes the annotation process.
*
* @param pe The processing environment.
*
* @see javax.annotation.processing.Processor
*/
@Override
public void init(ProcessingEnvironment pe) {
_pe = pe;
_options = new I18NProcessorOptions(pe);
_i18n = new I18NProcessorMessages(pe.getMessager(), _options.getLocale(), _options.getVerbosity());
_visitor = new I18NVisitor(pe, _i18n);
_resWrtr = new HashMap<String, PrintWriter>();
}
/**
* Processes the current round of annotations.
*
* @param st The set of annotated element candidates.
* @param re The environment of the current processing round.
*
* @return True if the current set of element candidates were consumed by this processor.
*
* @see javax.annotation.processing.Processor
*/
@Override
public boolean process(Set<? extends TypeElement> st, RoundEnvironment re) {
if (re.processingOver()) {
_doFinished(re);
} else {
_doGenerating(st, re);
}
return true;
}
/* Called when processing has completed. This method is responsible for
* finalizing all the resource bundle property file writers as well as
* generating the catalog if required. */
private void _doFinished(RoundEnvironment re) {
assert (_resWrtr != null) : "Writers are uninitialized." /*NOI18N*/;
for (final PrintWriter writer : _resWrtr.values()) {
writer.close();
}
_generateCatalog();
}
/* Apply the visitor to each annotated element in the current processing
* round. When all elements have been visited and their information collected the information is then used to write
* the resource bundle
* property file entries. */
private void _doGenerating(Set<? extends TypeElement> st, RoundEnvironment re) {
Class<I18NMessage> mClz = I18NMessage.class;
for (final Element e : re.getElementsAnnotatedWith(mClz)) {
e.accept(_visitor, null);
}
Class<I18NMessages> wClz = I18NMessages.class;
for (final Element e : re.getElementsAnnotatedWith(wClz)) {
e.accept(_visitor, null);
}
_generateBundles();
}
/* Write the resource bundle property file entries for the current list of
* visited elements. */
private void _generateBundles() {
List<MsgInfo> msgInfo = _visitor.getMsgInfo();
assert (msgInfo != null) : "Info may be empty but not null" /*NOI18N*/;
I18NObfuscator obfuscator = _options.getObfuscator();
for (final I18NVisitor.MsgInfo info : _visitor.getMsgInfo()) {
String name = info.getBaseName();
PrintWriter pw = _resWrtr.get(name);
if (pw == null) {
pw = _getResourceWriter(name, info.getPkg());
_resWrtr.put(name, pw);
}
if (!info.getHelp().isEmpty()) {
pw.println(info.getHelp());
}
String key = info.getKey();
assert ((key != null) && !key.isEmpty()) : "Null key." /*NOI18N*/;
String value = info.getValue(obfuscator != null);
if (value == null) {
value = EMPTY_STRING;
}
if (obfuscator != null) {
value = obfuscator.obfuscate(value);
}
pw.println(key + "=" + value);
} // end for
}
/* Generate the catalog if it hasn't been disabled. The catalog is a
* convenience class written out by the processor that may be used by
* developers to simplify access to their generated bundles. */
private void _generateCatalog() {
if (!_options.isSuppressCatalog()) {
String catName = _options.getCatalogName();
int sepIdx = catName.lastIndexOf(DOT_SEPARATOR);
String clz = (sepIdx >= 0) ? catName.substring(sepIdx + 1) : catName;
String pkg = (sepIdx >= 0) ? catName.substring(0, sepIdx) : null;
if (pkg != null) {
pkg = formatTemplate(CATALOG_PKGENTRY, pkg);
} else {
pkg = EMPTY_STRING;
}
String now = new Date().toString();
String data = _generateCatalogEntries();
String catalog = formatTemplate(CATALOG_BODY, now, pkg, clz, data);
PrintWriter catWrtr = _getSourceWriter(catName);
catWrtr.append(catalog);
catWrtr.flush();
catWrtr.close();
}
}
/* Helper method to iterate over all the collected data and generate the
* text replacement for the catalog template entries placeholder. */
private String _generateCatalogEntries() {
Set<String> seen = new HashSet<String>();
StringBuilder sb = new StringBuilder();
for (final I18NVisitor.MsgInfo info : _visitor.getMsgInfo()) {
String pckage = info.getPkg();
String baseName = info.getBaseName();
if (info.isLocalized()) {
String locale = UNDERSCORE.concat(info.getLocale());
baseName = baseName.replaceAll(locale, EMPTY_STRING);
}
String qName = pckage.concat(DOT_SEPARATOR).concat(baseName);
if (qName.endsWith(PROPERTY_FILE_SUFFIX)) {
int i = qName.length() - PROPERTY_FILE_SUFFIX.length();
qName = qName.substring(0, i);
}
if (!seen.contains(qName)) {
String path = qName.replaceAll(PKG_SEP_REGEX, FORWARD_SLASH);
if (path.startsWith(FORWARD_SLASH)) {
path = path.substring(1);
}
String key = info.getCatalogKey();
String entry = formatTemplate(CATALOG_MAPENTRY, key, path);
sb.append(entry);
seen.add(qName);
}
} // end for
String data = sb.toString();
return data;
}
/* Helper method that returns a resource file writer for the file having the
* indicated name and package. */
private PrintWriter _getResourceWriter(String name, String pkg) {
Filer filer = _pe.getFiler();
Element[] siblings = null;
Location loc = StandardLocation.CLASS_OUTPUT;
try {
return _getWriter(filer.createResource(loc, pkg, name, siblings), ASCII_ENCODING);
} catch (IOException ex) {
String msg = _i18n.record(Kind.ERROR, CANNOT_OPEN_BUNDLE);
throw new RuntimeException(msg, ex);
}
}
/* Helper method that returns a resource file writer for the file having the
* indicated fully qualified name. */
private PrintWriter _getSourceWriter(String qName) {
Filer filer = _pe.getFiler();
Element[] elements = null;
try {
return _getWriter(filer.createSourceFile(qName, elements), UTF8_ENCODING);
} catch (IOException ex) {
String msg = _i18n.record(Kind.ERROR, CANNOT_OPEN_SOURCE);
throw new RuntimeException(msg, ex);
}
}
/* Helper method that obtains a record writer for a given file object. */
private PrintWriter _getWriter(FileObject file, String encoding) throws IOException {
OutputStream os = file.openOutputStream();
OutputStreamWriter osw;
try {
osw = new OutputStreamWriter(os, encoding);
} catch (UnsupportedEncodingException ex) {
osw = new OutputStreamWriter(os, Charset.forName(encoding));
}
return new PrintWriter(osw);
}
}
/* Copyright Tom Valine 2002,2014 All Rights Reserved. ****************************************************************/