All Downloads are FREE. Search and download functionalities are using the official Maven repository.

auto.parse.processor.EclipseHack Maven / Gradle / Ivy

/*
 * Copyright (C) 2013 Google, Inc.
 *
 * Licensed 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 auto.parse.processor;


import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

/**
 * Works around an Eclipse bug where methods are sorted into alphabetical order before being given
 * to annotation processors. Unfortunately this seems to be deeply built in to the JDT compiler
 * that Eclipse uses. The bug has been open for over three years with no progress.
 * 

* To work around the problem, we access Eclipse-specific APIs to find the original source code of * the class with the {@code @AutoParse} annotation, and we do just enough parsing of that code to * be able to pick out the abstract method declarations so we can determine their order. The code * to access Eclipse-specific APIs will fail in environments other than Eclipse (for example, javac) * and the methods will be left in the order they came in, which in these other environments should * already be the correct order. *

* This is obviously a giant hack, and the right thing would be for the Eclipse compiler to be * fixed. The approach here works, but is vulnerable to future changes in the Eclipse API. If * {@code @AutoParse} constructor calls like {@code new AndroidAutoParse_Foo(...)} suddenly start being * redlined in a new Eclipse version then the likely cause is that the APIs have changed and this * hack will need to be updated to track the change. * * @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=300408 * * @author Éamonn McManus */ class EclipseHack { static final String ENABLING_OPTION = "auto.parse.EclipseHackTest"; private final ProcessingEnvironment processingEnv; private final boolean eclipseHackTest; EclipseHack(ProcessingEnvironment processingEnv) { boolean eclipseHackTest = processingEnv.getOptions().containsKey(ENABLING_OPTION); this.processingEnv = eclipseHackTest ? new EclipseProcessingEnvironment(processingEnv) : processingEnv; this.eclipseHackTest = eclipseHackTest; } // Fake implementation of ProcessingEnvironment that looks like Eclipse's, for testing only. private static class EclipseProcessingEnvironment implements ProcessingEnvironment { private final ProcessingEnvironment processingEnv; EclipseProcessingEnvironment(ProcessingEnvironment processingEnv) { this.processingEnv = processingEnv; } @SuppressWarnings("unused") // accessed via reflection public EclipseIFile getEnclosingIFile(Element element) { return new EclipseIFile(processingEnv, (TypeElement) element); } @Override public Map getOptions() { return processingEnv.getOptions(); } @Override public Messager getMessager() { return processingEnv.getMessager(); } @Override public Filer getFiler() { return processingEnv.getFiler(); } @Override public Elements getElementUtils() { return processingEnv.getElementUtils(); } @Override public Types getTypeUtils() { return processingEnv.getTypeUtils(); } @Override public SourceVersion getSourceVersion() { return processingEnv.getSourceVersion(); } @Override public Locale getLocale() { return processingEnv.getLocale(); } } private static class EclipseIFile { private final File file; EclipseIFile(ProcessingEnvironment processingEnv, TypeElement element) { Filer filer = processingEnv.getFiler(); // walk up the enclosing elements until you find a top-level element Element topLevel = element; while (topLevel.getEnclosingElement().getKind() != ElementKind.PACKAGE) { topLevel = topLevel.getEnclosingElement(); } try { FileObject resource = filer.getResource(StandardLocation.SOURCE_PATH, processingEnv.getElementUtils().getPackageOf(element).getQualifiedName(), topLevel.getSimpleName() + ".java"); this.file = new File(resource.toUri()); if (!file.canRead()) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Cannot find source code in file " + file, element); } } catch (IOException e) { throw new RuntimeException(e); } } @SuppressWarnings("unused") // accessed via reflection public String getCharset() { return Charset.defaultCharset().name(); } @SuppressWarnings("unused") // accessed via reflection public InputStream getContents() throws IOException { return new FileInputStream(file); } @SuppressWarnings("unused") // accessed via reflection public URI getRawLocationURI() { return file.toURI(); } } private static final Comparator ELEMENT_COMPARATOR = new Comparator() { @Override public int compare(ExecutableElement a, ExecutableElement b) { return a.getSimpleName().toString().compareTo(b.getSimpleName().toString()); } }; void sortMethodsIfSimulatingEclipse(List methods) { if (eclipseHackTest) { Collections.sort(methods, ELEMENT_COMPARATOR); } } /** * Reorders the properties (abstract methods) in the given list to correspond to the order found * by parsing the source code of the given type. In environments other than Eclipse this method * has no effect. */ void reorderProperties(List properties) { // Eclipse sorts methods in each class. Because of the way we construct the list, we will see // all the abstract property methods from a given class or interface consecutively. So we can // fix each sublist independently. int index = 0; while (index < properties.size()) { TypeElement owner = properties.get(index).owner(); int nextIndex = index + 1; while (nextIndex < properties.size() && properties.get(nextIndex).owner().equals(owner)) { nextIndex++; } List subList = properties.subList(index, nextIndex); reorderProperties(owner, subList); index = nextIndex; } } private void reorderProperties(TypeElement type, List properties) { PropertyOrderer propertyOrderer = getPropertyOrderer(type); if (propertyOrderer == null) { return; } final List order; try { order = propertyOrderer.determinePropertyOrder(); } catch (IOException e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, e.toString()); return; } // We expect that all the properties will be found, but if not then we won't try reordering. boolean allFound = true; for (AutoParseProcessor.Property property : properties) { allFound &= order.contains(property.toString()); } if (allFound) { // We successfully found the abstract methods corresponding to all the properties, so now // reorder the List to reflect the order of the methods. Comparator comparator = new Comparator() { @Override public int compare(AutoParseProcessor.Property a, AutoParseProcessor.Property b) { String aName = a.toString(); String bName = b.toString(); return order.indexOf(aName) - order.indexOf(bName); } }; Collections.sort(properties, comparator); } } private PropertyOrderer getPropertyOrderer(TypeElement type) { try { // If we are in Eclipse, then processingEnv will be an instance of // org.eclipse.jdt.internal.apt.pluggable.core.dispatch.IdeProcessingEnvImpl // and we can access its getEnclosingIFile method to obtain an Eclipse // org.eclipse.core.resources.IFile. Then we will access the // String getCharset(); // and // InputStream getContents(); // methods to access the whole source file that includes this class. // If the class in question has not changed since Eclipse last succeessfully compiled it // then the IFile will be the compiled class file rather than the source, and we will need // to read the order of the methods out of the class file. The method // URI getRawLocationURI(); // will tell us this because the URI will end with .class instead of .java. // If we are not in Eclipse then the reflection here will fail and we will return null, // which will mean that the caller won't try to reorder. Method getEnclosingIFile = processingEnv.getClass().getMethod("getEnclosingIFile", Element.class); final Object iFile = getEnclosingIFile.invoke(processingEnv, type); URI uri = (URI) iFile.getClass().getMethod("getRawLocationURI").invoke(iFile); if (uri.getPath().endsWith(".class")) { return new BinaryPropertyOrderer(uri); } else { Method getCharset = iFile.getClass().getMethod("getCharset"); final String charset = (String) getCharset.invoke(iFile); final Method getContents = iFile.getClass().getMethod("getContents"); Callable readerProvider = new Callable() { @Override public Reader call() throws Exception { InputStream inputStream = (InputStream) getContents.invoke(iFile); return new InputStreamReader(inputStream, charset); } }; return new SourcePropertyOrderer(type, readerProvider); } } catch (Exception e) { // Reflection failed, so we are presumably not in Eclipse. return null; } } private interface PropertyOrderer { List determinePropertyOrder() throws IOException; } private class SourcePropertyOrderer implements PropertyOrderer { private final TypeElement type; private final Callable readerProvider; /** * Constructs an object that scans the source code of the given type and returns the names of * all abstract methods directly declared in the type (not in nested types). The type itself may * be nested inside another class. Returns an empty list if the order could not be determined. * * @param type The type whose source is being scanned. * @param readerProvider A Callable that returns a Reader that will read the source of the whole * file in which the class is declared. */ SourcePropertyOrderer(TypeElement type, Callable readerProvider) { this.type = type; this.readerProvider = readerProvider; } @Override public List determinePropertyOrder() throws IOException { Reader sourceReader; try { sourceReader = readerProvider.call(); } catch (Exception e) { return Collections.emptyList(); } try { String packageName = TypeSimplifier.packageNameOf(type); String className = type.getQualifiedName().toString(); AbstractMethodExtractor extractor = new AbstractMethodExtractor(); JavaTokenizer tokenizer = new JavaTokenizer(sourceReader); Map> methodOrders = extractor.abstractMethods(tokenizer, packageName); if (methodOrders.containsKey(className)) { return methodOrders.get(className); } else { return Collections.emptyList(); } } finally { sourceReader.close(); } } } private class BinaryPropertyOrderer implements PropertyOrderer { private final URI classFileUri; BinaryPropertyOrderer(URI classFileUri) { this.classFileUri = classFileUri; } @Override public List determinePropertyOrder() throws IOException { InputStream inputStream = null; try { URL classFileUrl = classFileUri.toURL(); inputStream = classFileUrl.openStream(); AbstractMethodLister lister = new AbstractMethodLister(inputStream); return lister.abstractNoArgMethods(); } finally { if (inputStream != null) { inputStream.close(); } } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy