1 /*
   2  * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package jdk.vm.ci.options.processor;
  24 
  25 import java.io.*;
  26 import java.util.*;
  27 
  28 import javax.annotation.processing.*;
  29 import javax.lang.model.*;
  30 import javax.lang.model.element.*;
  31 import javax.lang.model.type.*;
  32 import javax.lang.model.util.*;
  33 import javax.tools.Diagnostic.Kind;
  34 
  35 import jdk.vm.ci.options.*;
  36 
  37 import javax.tools.*;
  38 
  39 /**
  40  * Processes static fields annotated with {@link Option}. An {@link OptionDescriptors}
  41  * implementation is generated for each top level class containing at least one such field. The name
  42  * of the generated class for top level class {@code com.foo.Bar} is
  43  * {@code com.foo.Bar_OptionDescriptors}.
  44  */
  45 @SupportedAnnotationTypes({"jdk.vm.ci.options.Option"})
  46 public class OptionProcessor extends AbstractProcessor {
  47 
  48     @Override
  49     public SourceVersion getSupportedSourceVersion() {
  50         return SourceVersion.latest();
  51     }
  52 
  53     private final Set<Element> processed = new HashSet<>();
  54 
  55     private void processElement(Element element, OptionsInfo info) {
  56 
  57         if (!element.getModifiers().contains(Modifier.STATIC)) {
  58             processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element);
  59             return;
  60         }
  61 
  62         Option annotation = element.getAnnotation(Option.class);
  63         assert annotation != null;
  64         assert element instanceof VariableElement;
  65         assert element.getKind() == ElementKind.FIELD;
  66         VariableElement field = (VariableElement) element;
  67         String fieldName = field.getSimpleName().toString();
  68 
  69         Elements elements = processingEnv.getElementUtils();
  70         Types types = processingEnv.getTypeUtils();
  71 
  72         TypeMirror fieldType = field.asType();
  73         if (fieldType.getKind() != TypeKind.DECLARED) {
  74             processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be of type " + OptionValue.class.getName(), element);
  75             return;
  76         }
  77         DeclaredType declaredFieldType = (DeclaredType) fieldType;
  78 
  79         TypeMirror optionValueType = elements.getTypeElement(OptionValue.class.getName()).asType();
  80         if (!types.isSubtype(fieldType, types.erasure(optionValueType))) {
  81             String msg = String.format("Option field type %s is not a subclass of %s", fieldType, optionValueType);
  82             processingEnv.getMessager().printMessage(Kind.ERROR, msg, element);
  83             return;
  84         }
  85 
  86         if (!field.getModifiers().contains(Modifier.STATIC)) {
  87             processingEnv.getMessager().printMessage(Kind.ERROR, "Option field must be static", element);
  88             return;
  89         }
  90 
  91         String help = annotation.help();
  92         if (help.length() != 0) {
  93             char firstChar = help.charAt(0);
  94             if (!Character.isUpperCase(firstChar)) {
  95                 processingEnv.getMessager().printMessage(Kind.ERROR, "Option help text must start with upper case letter", element);
  96                 return;
  97             }
  98         }
  99 
 100         String optionName = annotation.name();
 101         if (optionName.equals("")) {
 102             optionName = fieldName;
 103         }
 104 
 105         DeclaredType declaredOptionValueType = declaredFieldType;
 106         while (!types.isSameType(types.erasure(declaredOptionValueType), types.erasure(optionValueType))) {
 107             List<? extends TypeMirror> directSupertypes = types.directSupertypes(declaredFieldType);
 108             assert!directSupertypes.isEmpty();
 109             declaredOptionValueType = (DeclaredType) directSupertypes.get(0);
 110         }
 111 
 112         assert!declaredOptionValueType.getTypeArguments().isEmpty();
 113         String optionType = declaredOptionValueType.getTypeArguments().get(0).toString();
 114         if (optionType.startsWith("java.lang.")) {
 115             optionType = optionType.substring("java.lang.".length());
 116         }
 117 
 118         Element enclosing = element.getEnclosingElement();
 119         String declaringClass = "";
 120         String separator = "";
 121         Set<Element> originatingElementsList = info.originatingElements;
 122         originatingElementsList.add(field);
 123         while (enclosing != null) {
 124             if (enclosing.getKind() == ElementKind.CLASS || enclosing.getKind() == ElementKind.INTERFACE) {
 125                 if (enclosing.getModifiers().contains(Modifier.PRIVATE)) {
 126                     String msg = String.format("Option field cannot be declared in a private %s %s", enclosing.getKind().name().toLowerCase(), enclosing);
 127                     processingEnv.getMessager().printMessage(Kind.ERROR, msg, element);
 128                     return;
 129                 }
 130                 originatingElementsList.add(enclosing);
 131                 declaringClass = enclosing.getSimpleName() + separator + declaringClass;
 132                 separator = ".";
 133             } else {
 134                 assert enclosing.getKind() == ElementKind.PACKAGE;
 135             }
 136             enclosing = enclosing.getEnclosingElement();
 137         }
 138 
 139         info.options.add(new OptionInfo(optionName, help, optionType, declaringClass, field));
 140     }
 141 
 142     private void createFiles(OptionsInfo info) {
 143         String pkg = ((PackageElement) info.topDeclaringType.getEnclosingElement()).getQualifiedName().toString();
 144         Name topDeclaringClass = info.topDeclaringType.getSimpleName();
 145         Element[] originatingElements = info.originatingElements.toArray(new Element[info.originatingElements.size()]);
 146 
 147         createOptionsDescriptorsFile(info, pkg, topDeclaringClass, originatingElements);
 148     }
 149 
 150     private void createOptionsDescriptorsFile(OptionsInfo info, String pkg, Name topDeclaringClass, Element[] originatingElements) {
 151         String optionsClassName = topDeclaringClass + "_" + OptionDescriptors.class.getSimpleName();
 152 
 153         Filer filer = processingEnv.getFiler();
 154         try (PrintWriter out = createSourceFile(pkg, optionsClassName, filer, originatingElements)) {
 155 
 156             out.println("// CheckStyle: stop header check");
 157             out.println("// CheckStyle: stop line length check");
 158             out.println("// GENERATED CONTENT - DO NOT EDIT");
 159             out.println("// Source: " + topDeclaringClass + ".java");
 160             out.println("package " + pkg + ";");
 161             out.println("");
 162             out.println("import java.util.*;");
 163             out.println("import " + OptionDescriptors.class.getPackage().getName() + ".*;");
 164             out.println("");
 165             out.println("public class " + optionsClassName + " implements " + OptionDescriptors.class.getSimpleName() + " {");
 166 
 167             String desc = OptionDescriptor.class.getSimpleName();
 168 
 169             boolean needPrivateFieldAccessor = false;
 170             int i = 0;
 171             Collections.sort(info.options);
 172 
 173             out.println("    @Override");
 174             out.println("    public OptionDescriptor get(String value) {");
 175             out.println("        // CheckStyle: stop line length check");
 176             if (info.options.size() == 1) {
 177                 out.println("        if (value.equals(\"" + info.options.get(0).name + "\")) {");
 178             } else {
 179                 out.println("        switch (value) {");
 180             }
 181             for (OptionInfo option : info.options) {
 182                 String name = option.name;
 183                 String optionValue;
 184                 if (option.field.getModifiers().contains(Modifier.PRIVATE)) {
 185                     needPrivateFieldAccessor = true;
 186                     optionValue = "field(" + option.declaringClass + ".class, \"" + option.field.getSimpleName() + "\")";
 187                 } else {
 188                     optionValue = option.declaringClass + "." + option.field.getSimpleName();
 189                 }
 190                 String type = option.type;
 191                 String help = option.help;
 192                 String declaringClass = option.declaringClass;
 193                 Name fieldName = option.field.getSimpleName();
 194                 if (info.options.size() == 1) {
 195                     out.printf("            return %s.create(\"%s\", %s.class, \"%s\", %s.class, \"%s\", %s);\n", desc, name, type, help, declaringClass, fieldName, optionValue);
 196                 } else {
 197                     out.printf("            case \"" + name + "\": return %s.create(\"%s\", %s.class, \"%s\", %s.class, \"%s\", %s);\n", desc, name, type, help, declaringClass, fieldName,
 198                                     optionValue);
 199                 }
 200             }
 201             out.println("        }");
 202             out.println("        // CheckStyle: resume line length check");
 203             out.println("        return null;");
 204             out.println("    }");
 205             out.println();
 206             out.println("    @Override");
 207             out.println("    public Iterator<" + desc + "> iterator() {");
 208             out.println("        // CheckStyle: stop line length check");
 209             out.println("        List<" + desc + "> options = Arrays.asList(");
 210             for (OptionInfo option : info.options) {
 211                 String optionValue;
 212                 if (option.field.getModifiers().contains(Modifier.PRIVATE)) {
 213                     needPrivateFieldAccessor = true;
 214                     optionValue = "field(" + option.declaringClass + ".class, \"" + option.field.getSimpleName() + "\")";
 215                 } else {
 216                     optionValue = option.declaringClass + "." + option.field.getSimpleName();
 217                 }
 218                 String name = option.name;
 219                 String type = option.type;
 220                 String help = option.help;
 221                 String declaringClass = option.declaringClass;
 222                 Name fieldName = option.field.getSimpleName();
 223                 String comma = i == info.options.size() - 1 ? "" : ",";
 224                 out.printf("            %s.create(\"%s\", %s.class, \"%s\", %s.class, \"%s\", %s)%s\n", desc, name, type, help, declaringClass, fieldName, optionValue, comma);
 225                 i++;
 226             }
 227             out.println("        );");
 228             out.println("        // CheckStyle: resume line length check");
 229             out.println("        return options.iterator();");
 230             out.println("    }");
 231             if (needPrivateFieldAccessor) {
 232                 out.println("    private static " + OptionValue.class.getSimpleName() + "<?> field(Class<?> declaringClass, String fieldName) {");
 233                 out.println("        try {");
 234                 out.println("            java.lang.reflect.Field field = declaringClass.getDeclaredField(fieldName);");
 235                 out.println("            field.setAccessible(true);");
 236                 out.println("            return (" + OptionValue.class.getSimpleName() + "<?>) field.get(null);");
 237                 out.println("        } catch (Exception e) {");
 238                 out.println("            throw (InternalError) new InternalError().initCause(e);");
 239                 out.println("        }");
 240                 out.println("    }");
 241             }
 242             out.println("}");
 243         }
 244 
 245         try {
 246             createOptionsFile(pkg, topDeclaringClass.toString(), originatingElements);
 247         } catch (IOException e) {
 248             processingEnv.getMessager().printMessage(Kind.ERROR, e.getMessage(), info.topDeclaringType);
 249         }
 250     }
 251 
 252     private void createOptionsFile(String pkg, String relativeName, Element... originatingElements) throws IOException {
 253         String filename = "META-INF/jvmci.options/" + pkg + "." + relativeName;
 254         FileObject file = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", filename, originatingElements);
 255         PrintWriter writer = new PrintWriter(new OutputStreamWriter(file.openOutputStream(), "UTF-8"));
 256         writer.close();
 257     }
 258 
 259     protected PrintWriter createSourceFile(String pkg, String relativeName, Filer filer, Element... originatingElements) {
 260         try {
 261             // Ensure Unix line endings to comply with code style guide checked by Checkstyle
 262             JavaFileObject sourceFile = filer.createSourceFile(pkg + "." + relativeName, originatingElements);
 263             return new PrintWriter(sourceFile.openWriter()) {
 264 
 265                 @Override
 266                 public void println() {
 267                     print("\n");
 268                 }
 269             };
 270         } catch (IOException e) {
 271             throw new RuntimeException(e);
 272         }
 273     }
 274 
 275     static class OptionInfo implements Comparable<OptionInfo> {
 276 
 277         final String name;
 278         final String help;
 279         final String type;
 280         final String declaringClass;
 281         final VariableElement field;
 282 
 283         public OptionInfo(String name, String help, String type, String declaringClass, VariableElement field) {
 284             this.name = name;
 285             this.help = help;
 286             this.type = type;
 287             this.declaringClass = declaringClass;
 288             this.field = field;
 289         }
 290 
 291         @Override
 292         public int compareTo(OptionInfo other) {
 293             return name.compareTo(other.name);
 294         }
 295 
 296         @Override
 297         public String toString() {
 298             return declaringClass + "." + field;
 299         }
 300     }
 301 
 302     static class OptionsInfo {
 303 
 304         final Element topDeclaringType;
 305         final List<OptionInfo> options = new ArrayList<>();
 306         final Set<Element> originatingElements = new HashSet<>();
 307 
 308         public OptionsInfo(Element topDeclaringType) {
 309             this.topDeclaringType = topDeclaringType;
 310         }
 311     }
 312 
 313     private static Element topDeclaringType(Element element) {
 314         Element enclosing = element.getEnclosingElement();
 315         if (enclosing == null || enclosing.getKind() == ElementKind.PACKAGE) {
 316             assert element.getKind() == ElementKind.CLASS || element.getKind() == ElementKind.INTERFACE;
 317             return element;
 318         }
 319         return topDeclaringType(enclosing);
 320     }
 321 
 322     @Override
 323     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
 324         if (roundEnv.processingOver()) {
 325             return true;
 326         }
 327 
 328         Map<Element, OptionsInfo> map = new HashMap<>();
 329         for (Element element : roundEnv.getElementsAnnotatedWith(Option.class)) {
 330             if (!processed.contains(element)) {
 331                 processed.add(element);
 332                 Element topDeclaringType = topDeclaringType(element);
 333                 OptionsInfo options = map.get(topDeclaringType);
 334                 if (options == null) {
 335                     options = new OptionsInfo(topDeclaringType);
 336                     map.put(topDeclaringType, options);
 337                 }
 338                 processElement(element, options);
 339             }
 340         }
 341 
 342         boolean ok = true;
 343         Map<String, OptionInfo> uniqueness = new HashMap<>();
 344         for (OptionsInfo info : map.values()) {
 345             for (OptionInfo option : info.options) {
 346                 OptionInfo conflict = uniqueness.put(option.name, option);
 347                 if (conflict != null) {
 348                     processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate option names for " + option + " and " + conflict, option.field);
 349                     ok = false;
 350                 }
 351             }
 352         }
 353 
 354         if (ok) {
 355             for (OptionsInfo info : map.values()) {
 356                 createFiles(info);
 357             }
 358         }
 359 
 360         return true;
 361     }
 362 }