/* Copyright Tom Valine 2002,2014 All Rights Reserved. ****************************************************************/
package com.kre8orz.i18n.processor;
import com.kre8orz.i18n.annotation.I18NMessage;
import com.kre8orz.i18n.annotation.I18NMessages;
import com.kre8orz.i18n.annotation.I18NResourceBundle;
import java.util.*;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleElementVisitor7;
import javax.tools.Diagnostic.Kind;
import static com.kre8orz.i18n.processor.I18NProcessorConstants.*;
import static com.kre8orz.i18n.processor.I18NProcessorMessages.*;
/**
* The visitor used during annotation processing to collect the detailed information regarding messages and resource
* bundles. The visitor pattern is the method adopted by Java for the processing of elements during annotation
* processing.
*
* @author Tom Valine (thomas.valine@gmail.com)
* @see SimpleElementVisitor7
*/
final class I18NVisitor extends SimpleElementVisitor7<Void, Void> {
//~ Static fields/initializers *************************************************************************************
/** A table of hex digits. */
private static final char[] hexDigit = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
//~ Methods ********************************************************************************************************
private static String convertComment(String comments) {
StringBuilder sb = new StringBuilder("#");
int len = comments.length();
int current = 0;
int last = 0;
char[] uu = new char[6];
uu[0] = '\\';
uu[1] = 'u';
while (current < len) {
char c = comments.charAt(current);
if ((c > '\u00ff') || (c == '\n') || (c == '\r')) {
if (last != current) {
sb.append(comments.substring(last, current));
}
if (c > '\u00ff') {
uu[2] = toHex((c >> 12) & 0xf);
uu[3] = toHex((c >> 8) & 0xf);
uu[4] = toHex((c >> 4) & 0xf);
uu[5] = toHex(c & 0xf);
sb.append(new String(uu));
} else {
sb.append("\n");
if ((c == '\r') && (current != (len - 1)) && (comments.charAt(current + 1) == '\n')) {
current++;
}
if ((current == (len - 1)) ||
((comments.charAt(current + 1) != '#') && (comments.charAt(current + 1) != '!'))) {
sb.append("#");
}
}
last = current + 1;
}
current++;
} // end while
if (last != current) {
sb.append(comments.substring(last, current));
}
return sb.toString();
}
/**
* Convert a nibble to a hex character.
*
* @param nibble the nibble to convert.
*
* @return convert a nibble to a hex character.
*/
private static char toHex(int nibble) {
return hexDigit[(nibble & 0xF)];
}
//~ Instance fields ************************************************************************************************
private final I18NProcessorMessages _i18n;
private final List<MsgInfo> _msgInfo;
private final ProcessingEnvironment _pe;
//~ Constructors ***************************************************************************************************
/**
* Creates a new I18NVisitor object.
*
* @param pe The current processing environment supplied by the compiler or other processor host if not running
* via a compiler.
* @param i18n An instance of the processor messages class. This class is used to retrieve and format log messages
* if needed.
*/
I18NVisitor(ProcessingEnvironment pe, I18NProcessorMessages i18n) {
_pe = pe;
_msgInfo = new ArrayList<MsgInfo>();
_i18n = i18n;
}
//~ Methods ********************************************************************************************************
/**
* Used to obtain the complete list of processed annotation information. This information is used by the annotation
* processor during bundle and catalog generation.
*
* @return The list of processed annotation information.
*/
public List<MsgInfo> getMsgInfo() {
return Collections.unmodifiableList(_msgInfo);
}
/**
* Dispatch method for variable elements. This method specifically processes <tt>I18NMessages</tt> and <tt>
* I18NMessage</tt> annotations. As part of this processing, the annotated field is checked to ensure that it is a
* <tt>String</tt> constant (e.g. static final String).
*
* @param ele {@inheritDoc}
* @param par {@inheritDoc}
*
* @return Nothing as the visitor simply processes the annotations during round processing.
*
* @see javax.lang.model.util.SimpleElementVisitor6
*/
@Override
public Void visitVariable(VariableElement ele, Void par) {
I18NMessage msg = ele.getAnnotation(I18NMessage.class);
I18NMessages msgs = ele.getAnnotation(I18NMessages.class);
if (_checkModifiers(ele)) {
if (msgs != null) {
for (I18NMessage ann : msgs.value()) {
_msgInfo.add(new MsgInfo(ele, ann));
}
}
if (msg != null) {
_msgInfo.add(new MsgInfo(ele, msg));
}
} else {
_i18n.record(Kind.WARNING, STRING_CONSTANT_REQUIRED, ele);
}
return null;
}
/* Helper method used to ensure that the i18n annotations are attached only
* to string constants. */
private boolean _checkModifiers(VariableElement ele) {
Set<Modifier> modifiers = ele.getModifiers();
return modifiers.contains(Modifier.FINAL) && modifiers.contains(Modifier.STATIC) &&
(ele.getConstantValue() != null) && !ele.asType().getKind().isPrimitive();
}
private String convertValue(String theString, boolean isKey) {
int len = theString.length();
int bufLen = len * 2;
if (bufLen < 0) {
bufLen = Integer.MAX_VALUE;
}
StringBuilder outBuffer = new StringBuilder(bufLen);
for (int x = 0; x < len; x++) {
char aChar = theString.charAt(x);
// Handle common case first, selecting largest block that
// avoids the specials below
if ((aChar > 61) && (aChar < 127)) {
if (aChar == '\\') {
outBuffer.append('\\');
outBuffer.append('\\');
continue;
}
outBuffer.append(aChar);
continue;
}
switch (aChar) {
case ' ': {
if ((x == 0) || isKey) {
outBuffer.append('\\');
}
outBuffer.append(' ');
break;
}
case '\t':
case '\n':
case '\r':
case '\f':
case '=':
case ':':
case '#':
case '!': {
outBuffer.append(aChar);
break;
}
default: {
if ((aChar < 0x0020) || (aChar > 0x007e)) {
outBuffer.append('\\');
outBuffer.append('u');
outBuffer.append(toHex((aChar >> 12) & 0xF));
outBuffer.append(toHex((aChar >> 8) & 0xF));
outBuffer.append(toHex((aChar >> 4) & 0xF));
outBuffer.append(toHex(aChar & 0xF));
} else {
outBuffer.append(aChar);
}
}
} // end switch
} // end for
return outBuffer.toString();
}
//~ Inner Classes **************************************************************************************************
/**
* Helper class used to elaborate an individual <tt>I18NMessage</tt> annotation and encapsulate the distilled
* message information.
*
* @author Tom Valine (thomas.valine@gmail.com)
*/
class MsgInfo {
private String _baseName;
private String _catalogKey;
private final String _help;
private final String _key;
private final String _locale;
private String _pkg;
private final String _value;
private MsgInfo(VariableElement ele, I18NMessage msg) {
_calculateNames(ele, msg);
_calculateCatalogKey(ele);
this._key = ele.getConstantValue().toString();
this._value = msg.value();
this._help = msg.note();
this._locale = msg.locale();
}
/**
* Returns the base name of the bundle to which this message should belong.
*
* @return The base name of the bundle to which this message should belong.
*/
String getBaseName() {
return _baseName;
}
/**
* Returns the catalog key that can be used to obtain the path to the resource bundle to which this message
* belongs. This resolves to the simple name of the interface class in which the original annotated field was
* declared.
*
* @return The bundle key used to obtain the path to the generated resource bundle to which this message
* belongs.
*/
String getCatalogKey() {
return _catalogKey;
}
/**
* Returns the help note specified in the message annotation. This value is used as the comment for the message
* in the corresponding generated resource bundle.
*
* @return The help note specified for the message.
*/
String getHelp() {
return ((_help == null) || _help.isEmpty()) ? "" : convertComment(_help);
}
/**
* Returns the bundle key name for this message. This value is used directly as the key name for the message in
* the generated bundle class.
*
* @return The bundle key name for this message.
*/
String getKey() {
return convertValue(_key, true);
}
/**
* Returns the locale for which this message translation is to be used. This value is used to determine which
* locale specific generated bundle should contain this message translation.
*
* @return The locale for which this message translation is to be used.
*/
String getLocale() {
return _locale;
}
/**
* Returns the package name for the generated bundle in which this message is contained.
*
* @return The package name for the generated bundle.
*/
String getPkg() {
return _pkg;
}
/**
* Returns the unformatted message string.
*
* @param rawValue Return a raw value rather than escaped Unicode.
*
* @return The unformatted message string.
*/
String getValue(boolean rawValue) {
return rawValue ? _value : convertValue(_value, false);
}
/**
* Indicates whether or not a message is in fact a locale specific translation of the base message.
*
* @return True if the message is a locale specific translation.
*/
boolean isLocalized() {
return (!_locale.trim().isEmpty());
}
/* Helper method used to calculate the value of the key used in the
* catalog class. */
private void _calculateCatalogKey(VariableElement ele) {
String catKey = ele.getEnclosingElement().getSimpleName().toString();
PackageElement pack = _pe.getElementUtils().getPackageOf(ele);
if (!pack.isUnnamed()) {
String sep = DOT_SEPARATOR;
catKey = pack.getQualifiedName() + sep + catKey;
}
this._catalogKey = catKey;
}
/* Helper method used to calculate the base name and package name for
* the bundle into which this annotation will have it's information
* written to. */
private void _calculateNames(VariableElement ele, I18NMessage msg) {
Class<I18NResourceBundle> bClz = I18NResourceBundle.class;
I18NResourceBundle rb = ele.getEnclosingElement().getAnnotation(bClz);
String name;
String pack;
if (rb != null) {
name = rb.bundleName();
pack = rb.packageName();
} else {
Elements eUtils = _pe.getElementUtils();
pack = eUtils.getPackageOf(ele).getQualifiedName().toString();
name = ele.getEnclosingElement().getSimpleName().toString();
}
if (!msg.locale().isEmpty()) {
name += UNDERSCORE + msg.locale();
}
name += PROPERTY_FILE_SUFFIX;
_baseName = name;
_pkg = pack;
}
}
}
/* Copyright Tom Valine 2002,2014 All Rights Reserved. ****************************************************************/