/** * Copyright 2015 Santhosh Kumar Tekuri * * The JLibs authors license 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 jlibs.core.util.i18n; import jlibs.core.annotation.processing.AnnotationError; import jlibs.core.annotation.processing.AnnotationProcessor; import jlibs.core.annotation.processing.Environment; import jlibs.core.annotation.processing.Printer; import jlibs.core.lang.StringUtil; import jlibs.core.lang.model.ModelUtil; import org.kohsuke.MetaInfServices; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.lang.model.type.DeclaredType; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.tools.FileObject; import javax.tools.StandardLocation; import java.io.BufferedWriter; import java.io.IOException; import java.text.MessageFormat; import java.util.*; import static jlibs.core.annotation.processing.Printer.MINUS; import static jlibs.core.annotation.processing.Printer.PLUS; import static jlibs.core.util.i18n.PropertiesUtil.*; /** * @author Santhosh Kumar T */ @SuppressWarnings({"unchecked"}) @SupportedAnnotationTypes({ "jlibs.core.util.i18n.ResourceBundle", "jlibs.core.util.i18n.Bundle" }) @SupportedSourceVersion(SourceVersion.RELEASE_6) @SupportedOptions("ResourceBundle.basename") @MetaInfServices(Processor.class) public class BundleAnnotationProcessor extends AnnotationProcessor{ private static String basename; private static class Info{ private String pakage; private BufferedWriter props; private Map<String, Element> entries = new HashMap<String, Element>(); private Interfaces interfaces; private Bundles bundles; public Info(Element element, AnnotationMirror mirror) throws IOException{ pakage = ModelUtil.getPackage(element); if(ModelUtil.exists(pakage, basename+".properties")) throw new AnnotationError(element, mirror, basename+".properties in package "+pakage+" already exists in source path"); FileObject resource = Environment.get().getFiler().createResource(StandardLocation.CLASS_OUTPUT, pakage, basename+".properties"); props = new BufferedWriter(resource.openWriter()); } public void addResourceBundle(TypeElement clazz) throws IOException{ if(interfaces==null) interfaces = new Interfaces(entries); interfaces.add(clazz); } public void addBundle(Element element) throws IOException{ if(bundles==null) bundles = new Bundles(); bundles.add(element); } public void generate() throws IOException{ writeComments(props, " DON'T EDIT THIS FILE. THIS IS GENERATED BY JLIBS"); writeComments(props, " @author Santhosh Kumar T"); props.newLine(); if(interfaces!=null){ interfaces.generateClass(basename); interfaces.generateProperties(props); } if(bundles!=null) bundles.generateProperties(entries, props); close(); } public void close() throws IOException{ if(interfaces!=null){ interfaces.close(); interfaces = null; } if(props!=null){ props.close(); props = null; } } } private static Map<String, Info> infos = new HashMap<String, Info>(); @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){ basename = Environment.get().getOptions().get("ResourceBundle.basename"); if(basename==null) basename = "Bundle"; try{ for(TypeElement annotation: annotations){ if(annotation.getQualifiedName().contentEquals(ResourceBundle.class.getName())){ for(Element elem: roundEnv.getElementsAnnotatedWith(annotation)){ TypeElement c = (TypeElement)elem; String pakage = ModelUtil.getPackage(c); if(c.getKind()!=ElementKind.INTERFACE) throw new AnnotationError(elem, ResourceBundle.class.getName()+" annotation can be applied only for interface"); Info info = infos.get(pakage); if(info==null) infos.put(pakage, info=new Info(c, ModelUtil.getAnnotationMirror(c, ResourceBundle.class))); info.addResourceBundle(c); } }else{ for(Element elem: roundEnv.getElementsAnnotatedWith(annotation)){ String pakage = ModelUtil.getPackage(elem); Info info = infos.get(pakage); if(info==null) infos.put(pakage, info=new Info(elem, ModelUtil.getAnnotationMirror(elem, Bundle.class))); info.addBundle(elem); } } } for(Info info: infos.values()) info.generate(); }catch(AnnotationError error){ error.report(); }catch(IOException ex){ throw new RuntimeException(ex); }finally{ for(Info info : infos.values()){ try{ info.close(); }catch(IOException ignore){ // ignore } } infos.clear(); } return true; } } class Interfaces{ private List<String> interfaces = new ArrayList<String>(); private Printer printer; private Map<String, ExecutableElement> entries; private Map<Element, Map<String, ExecutableElement>> classes = new HashMap<Element, Map<String, ExecutableElement>>(); @SuppressWarnings({"unchecked"}) Interfaces(Map entries){ this.entries = entries; } public void add(TypeElement clazz) throws IOException{ if(printer==null) printer = Printer.get(clazz, ResourceBundle.class, I18N.FORMAT); interfaces.add(clazz.getSimpleName().toString()); while(clazz!=null && !clazz.getQualifiedName().contentEquals(Object.class.getName())){ for(ExecutableElement method: ElementFilter.methodsIn(clazz.getEnclosedElements())) add(method); clazz = ModelUtil.getSuper(clazz); } } private void add(ExecutableElement method){ AnnotationMirror mirror = ModelUtil.getAnnotationMirror(method, Message.class); if(mirror==null) throw new AnnotationError(method, Message.class.getName()+" annotation is missing on this method"); if(!ModelUtil.isAssignable(method.getReturnType(), String.class)){ if(!ModelUtil.isAssignable(method.getReturnType(), Throwable.class)){ throw new AnnotationError(method, "method annotated with "+Message.class.getName()+ " must return java.lang.String or a subclass of java.lang.Throwable"); } Element element = ((DeclaredType)method.getReturnType()).asElement(); boolean foundValidConstructor = false; for(ExecutableElement constructor: ElementFilter.constructorsIn(element.getEnclosedElements())){ List<? extends VariableElement> params = constructor.getParameters(); if(params.size()==2){ if(ModelUtil.isAssignable(params.get(0).asType(), String.class) && ModelUtil.isAssignable(params.get(1).asType(), String.class)){ foundValidConstructor = true; break; } } } if(!foundValidConstructor){ String className = ModelUtil.toString(method.getReturnType(), false); throw new AnnotationError(method, "Constructor "+className+"(String errorCode, String message) not found"); } } String signature = ModelUtil.signature(method, false); for(ExecutableElement m : entries.values()){ if(signature.equals(ModelUtil.signature(m, false))) throw new AnnotationError(method, "clashes with similar method in "+m.getEnclosingElement()+" interface"); } AnnotationMirror messageMirror = ModelUtil.getAnnotationMirror(method, Message.class); String key = ModelUtil.getAnnotationValue(method, messageMirror, "key"); if(StringUtil.isEmpty(key)) key = method.getSimpleName().toString(); ExecutableElement clash = entries.put(key, method); Element interfase = method.getEnclosingElement(); if(clash!=null) throw new AnnotationError(method, "key '"+key+"' is already used by \""+ModelUtil.signature(clash, false)+"\" in "+ clash.getEnclosingElement()+" interface"); Map<String, ExecutableElement> methods = classes.get(interfase); if(methods==null) classes.put(interfase, methods=new HashMap<String, ExecutableElement>()); methods.put(key, method); } public void generateClass(String basename) throws IOException{ printer.printPackage(); printer.importClass(java.util.ResourceBundle.class); printer.importClass(MessageFormat.class); printer.importClass(LocaleContext.class); printer.emptyLine(true); printer.printClassDoc(); printer.println("@SuppressWarnings(\"unchecked\")"); printer.println("public class "+printer.generatedClazz +" implements "+StringUtil.join(interfaces.iterator(), ", ")+"{"); printer.indent++; printer.println("public static final "+printer.generatedClazz +" INSTANCE = new "+printer.generatedClazz +"();"); printer.emptyLine(true); printer.printlns( "private final ResourceBundle BUNDLE(){", PLUS, "return ResourceBundle.getBundle(\""+printer.generatedPakage.replace('.', '/')+"/"+basename+"\", LocaleContext.getLocale());", MINUS, "}" ); printer.emptyLine(true); for(Map.Entry<Element, Map<String, ExecutableElement>> methods : classes.entrySet()){ printer.emptyLine(true); printer.println("/*-------------------------------------------------[ "+methods.getKey().getSimpleName()+" ]---------------------------------------------------*/"); printer.emptyLine(true); for(Map.Entry<String, ExecutableElement> entry : methods.getValue().entrySet()){ String key = entry.getKey(); ExecutableElement method = entry.getValue(); printer.println("@Override"); boolean returnsException = ModelUtil.isAssignable(method.getReturnType(), Throwable.class); String returnType = returnsException ? ModelUtil.toString(method.getReturnType(), false) : "String" ; printer.print("public "+returnType+" "+method.getSimpleName()+"("); int i = 0; StringBuilder params = new StringBuilder(); for(VariableElement param : method.getParameters()){ String paramName = param.getSimpleName().toString(); params.append(", "); if(i>0) printer.print(", "); params.append(paramName); printer.print(ModelUtil.toString(param.asType(), false)+" "+paramName); i++; } if(params.length()==0) params.append(", new Object[0]"); printer.println("){"); printer.indent++; final String message = "MessageFormat.format(BUNDLE().getString(\""+key+"\")"+params+")"; if(returnsException){ String prefix = printer.generatedPakage; String option = Environment.get().getOptions().get("ResourceBundle.ignorePackageCount"); int packageIgnoreCount = option==null ? 2 : Integer.parseInt(option); if(packageIgnoreCount==-1) prefix = ""; else{ for(int j=0; j<packageIgnoreCount; j++){ int dot = prefix.indexOf("."); if(dot==-1) break; prefix = prefix.substring(dot+1); } } String errorCode = StringUtil.capitalize(key); if(!prefix.isEmpty()) errorCode = prefix+"."+errorCode; printer.println(" return new "+returnType+"(\""+errorCode+"\", "+message+");"); }else printer.println("return "+message+";"); printer.indent--; printer.println("}"); } } printer.indent--; printer.println("}"); close(); } public void generateProperties(BufferedWriter props) throws IOException{ Elements elemUtil = Environment.get().getElementUtils(); for(Map.Entry<Element, Map<String, ExecutableElement>> methods : classes.entrySet()){ writeComments(props, "-------------------------------------------------[ "+methods.getKey().getSimpleName()+" ]---------------------------------------------------"); props.newLine(); for(Map.Entry<String, ExecutableElement> entry : methods.getValue().entrySet()){ String key = entry.getKey(); ExecutableElement method = entry.getValue(); String doc = elemUtil.getDocComment(method); String methodDoc = ModelUtil.getMethodDoc(doc); if(!StringUtil.isEmpty(methodDoc)) writeComments(props, " "+methodDoc); int i = 0; Map<String, String> paramDocs = ModelUtil.getMethodParamDocs(doc); for(VariableElement param : method.getParameters()){ String paramName = param.getSimpleName().toString(); String paramDoc = paramDocs.get(paramName); if(StringUtil.isEmpty(paramDoc)) writeComments(props, " {"+i+"} "+paramName); else writeComments(props, " {"+i+"} "+paramName+" ==> "+paramDoc); i++; } AnnotationMirror messageMirror = ModelUtil.getAnnotationMirror(method, Message.class); String value = ModelUtil.getAnnotationValue(method, messageMirror, "value"); try{ new MessageFormat(value); }catch(IllegalArgumentException ex){ throw new AnnotationError(method, messageMirror, ModelUtil.getRawAnnotationValue(method, messageMirror, "value"), "Invalid Message Format: "+ex.getMessage()); } NavigableSet<Integer> args = findArgs(value); int argCount = args.size()==0 ? 0 : (args.last()+1); if(argCount!=method.getParameters().size()) throw new AnnotationError(method, "no of args in message format doesn't match with the number of parameters this method accepts"); for(i=0; i<argCount; i++){ if(!args.remove(i)) throw new AnnotationError(method, messageMirror, "{"+i+"} is missing in message"); } writeProperty(props, key, value); props.newLine(); } } } public void close() throws IOException{ if(printer!=null){ printer.close(); printer = null; } } } class Bundles{ private Map<Element, List<Element>> classes = new HashMap<Element, List<Element>>(); public void add(Element element){ Element container = element.getEnclosingElement(); if(container instanceof PackageElement) container = element; List<Element> list = classes.get(container); if(list==null) classes.put(container, list=new ArrayList<Element>()); list.add(element); } @SuppressWarnings({"unchecked"}) public void generateProperties(Map<String, Element> entries, BufferedWriter props) throws IOException{ for(Map.Entry<Element, List<Element>> entry: classes.entrySet()){ writeComments(props, "-------------------------------------------------[ "+entry.getKey().getSimpleName()+" ]---------------------------------------------------"); props.newLine(); for(Element method : entry.getValue()){ AnnotationMirror mirror = ModelUtil.getAnnotationMirror(method, Bundle.class); for(AnnotationValue value: (Collection<AnnotationValue>)ModelUtil.getAnnotationValue(method, mirror, "value")){ AnnotationMirror entryMirror = (AnnotationMirror)value.getValue(); String rhs = ModelUtil.getAnnotationValue(method, entryMirror, "rhs"); if(rhs.length()>0){ String lhs = ModelUtil.getAnnotationValue(method, entryMirror, "lhs"); if(lhs.length()==0){ String hintName = ModelUtil.getAnnotationValue(method, entryMirror, "hintName"); if(hintName.length()==0){ Hint hint = Hint.valueOf(((VariableElement)ModelUtil.getAnnotationValue(method, entryMirror, "hint")).getSimpleName().toString()); if(hint==Hint.NONE) throw new AnnotationError(""); hintName = hint.key(); } lhs = method.getEnclosingElement().getSimpleName()+"."+method.getSimpleName()+"."+hintName; } Element clash = entries.put(lhs, method); if(clash!=null){ String signature; if(clash instanceof ExecutableElement) signature = ModelUtil.signature((ExecutableElement)clash, false); else if(clash instanceof TypeElement) throw new AnnotationError(method, "key '"+lhs+"' is already used by \""+((TypeElement)clash).getQualifiedName()); else signature = clash.getSimpleName().toString(); throw new AnnotationError(method, "key '"+lhs+"' is already used by \""+signature+"\" in "+ clash.getEnclosingElement()); } writeProperty(props, lhs, rhs); }else{ String comment = ModelUtil.getAnnotationValue(method, entryMirror, "value"); if(comment.length()>0) writeComments(props, comment); else props.newLine(); } } } props.newLine(); } } }