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

juzu.impl.compiler.ProcessingContext Maven / Gradle / Ivy

There is a newer version: 1.2.0
Show newest version
/*
 * Copyright 2013 eXo Platform SAS
 *
 * 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 juzu.impl.compiler;

import juzu.impl.common.Name;
import juzu.impl.common.FileKey;
import juzu.impl.common.Path;
import juzu.impl.compiler.file.JavaFileObjectImpl;
import juzu.impl.fs.spi.ReadFileSystem;
import juzu.impl.fs.spi.disk.DiskFileSystem;
import juzu.impl.common.Logger;
import juzu.impl.common.Spliterator;
import juzu.impl.common.Tools;

import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.NullType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.concurrent.Callable;
import java.util.logging.Level;

/** @author Julien Viet */
public class ProcessingContext extends Logger implements Filer, Elements, Types {

  /** The base classloader for service loading, this static allow to change it for unit tests. */
  public static ClassLoader baseCL = ProcessingContext.class.getClassLoader();

  /** . */
  private static final MessageCode UNEXPECTED_ERROR = new MessageCode("UNEXPECTED_ERROR", "Unexpected error: %1$s");

  /**
   * We need two locations as the {@link javax.tools.StandardLocation#SOURCE_PATH} is not supported in eclipse ide (see
   * https://bugs.eclipse.org/bugs/show_bug.cgi?id=341298), however the {@link javax.tools.StandardLocation#CLASS_OUTPUT}
   * seems to work fairly well.
   */
  private static final StandardLocation[] RESOURCE_LOCATIONS = {StandardLocation.SOURCE_PATH, StandardLocation.CLASS_OUTPUT};

  /** . */
  private ProcessingEnvironment env;

  /** . */
  public static final Logger log = BaseProcessor.getLogger(ProcessingContext.class);

  /** Set for eclipse environment. */
  private final DiskFileSystem sourcePath;

  /** . */
  private final ProcessingTool tool;

  /** The classloader for loading service via ServiceLoader. */
  private final ClassLoader serviceCL;

  /** The resources accessed, we need to have this map because some resources may only open one time (cf eclipse filer). */
  private Map resources;

  /** . */
  private static final char PATH_SEPARATOR_CHAR = System.getProperty("path.separator").charAt(0);

  public ProcessingContext(ProcessingEnvironment env) {
    ProcessingTool tool;
    if (env.getMessager().getClass().getName().startsWith("org.eclipse.jdt")) {
      tool = ProcessingTool.ECLIPSE_IDE;
    }
    else {
      tool = ProcessingTool.JAVAC;
    }

    //
    ClassLoader serviceCL = baseCL;

    //
    DiskFileSystem sourcePath = null;
    try {
      // As first attempt we tried to use the classpath since eclipse would copy the template to this location
      // but that could a chicken egg problem as a template is coped in the classpath only if the compilation
      // is successfull and sometimes a controller references a template literal that is generated from the
      // template source
      ClassLoader cl = env.getClass().getClassLoader();
      Class eclipseImplClass = cl.loadClass("org.eclipse.jdt.internal.apt.pluggable.core.dispatch.IdeProcessingEnvImpl");
      if (eclipseImplClass.isInstance(env)) {
        Method getJavaProject = eclipseImplClass.getMethod("getJavaProject");
        Object javaProject = getJavaProject.invoke(env);
        Class aptConfigClass = cl.loadClass("org.eclipse.jdt.apt.core.util.AptConfig");
        Class javaProjectClass = cl.loadClass("org.eclipse.jdt.core.IJavaProject");
        Method getProcessorOptionsMethod = aptConfigClass.getMethod("getProcessorOptions", javaProjectClass);
        Map options = (Map)getProcessorOptionsMethod.invoke(null, javaProject);
        log.info("Retrieved options " + options);

        //
        String sp = options.get("-sourcepath");
        log.info("Found sourcepath " + sp);
        if (sp != null) {
          // We take the first value
          Spliterator split = new Spliterator(sp, PATH_SEPARATOR_CHAR);
          if (split.hasNext()) {
            File root = new File(split.next());
            if (root.isDirectory()) {
              sourcePath = new DiskFileSystem(root);
            }
          }
        }

        // Building service class loader, this works better in eclipse specially with m2e and
        // the externally loaded plugins
        String cp = options.get("-classpath");
        log.info("Found classpath " + cp);
        if (cp != null) {
          ArrayList urls = new ArrayList();
          for (String s : Spliterator.split(cp, PATH_SEPARATOR_CHAR)) {
            File f = new File(s);
            if (f.exists()) {
              if (f.isFile() && f.getName().endsWith(".jar") || f.isDirectory()) {
                urls.add(f.toURI().toURL());
              }
            }
          }
          serviceCL = new URLClassLoader(urls.toArray(new URL[urls.size()]), serviceCL);
        }
      }
    }
    catch (Exception ignore) {
    }

    //
    log.info("Using processing tool " + tool);
    log.info("Using processing " + env);
    log.info("Using source path " + sourcePath);

    //
    this.env = env;
    this.sourcePath = sourcePath;
    this.tool = tool;
    this.serviceCL = serviceCL;
    this.resources = null;
  }

  // Various stuff ****************************************************************************************************

  public void report(Diagnostic.Kind kind,
                     CharSequence msg,
                     Element e,
                     AnnotationMirror a,
                     AnnotationValue v) {
    tool.report(env.getMessager(), kind, msg, e, a, v);
  }

  public ProcessingEnvironment getEnv() {
    return env;
  }
  
  public boolean isCopyFromSourcesExternallyManaged() {
      return tool.isCopyFromSourcesExternallyManaged();
  }

  public  E get(ElementHandle handle) {
    return handle.get(env);
  }

  public  T executeWithin(ElementHandle element, Callable callable) throws ProcessingException {
    return executeWithin(element.get(env), callable);
  }

  public  T executeWithin(Element element, Callable callable) throws ProcessingException {
    try {
      return callable.call();
    }
    catch (ProcessingException e) {
      if (e.getElement() != null) {
        throw e;
      }
      else {
        throw new ProcessingException(element, null, e.getMessages()).initCause(e);
      }
    }
    catch (Exception e) {
      throw new ProcessingException(element, UNEXPECTED_ERROR, e.getMessage()).initCause(e);
    }
  }

  /** . */
  private Map, DiskFileSystem> sourcePathMap = new HashMap, DiskFileSystem>();

  /**
   * Returns the source path, this may return null.
   *
   * @param context the package element related
   * @return the source path
   * @throws IllegalArgumentException if the package cannot be resolved
   */
  public DiskFileSystem getSourcePath(ElementHandle.Package context) throws IllegalArgumentException {
    PackageElement element = context.get(env);
    if (element == null) {
      throw new IllegalArgumentException("Package element cannot be resolved " + context);
    }
    if (sourcePath != null) {
      log.info("Found eclipse source path " + sourcePath + " for package " + context.getPackageName());
      return sourcePath;
    }
    else {
      DiskFileSystem sourcePath = sourcePathMap.get(context);
      if (sourcePath == null) {
        try {
          log.info("Trying to find a native file system for package " + context.getPackageName());
          List annotations = element.getAnnotationMirrors();
          if (annotations.size() > 0) {
            log.info("Found package " + context.getPackageName() + " annotations " + annotations + " will use first one");
            AnnotationMirror annotation = annotations.get(0);
            ClassLoader cl = env.getClass().getClassLoader();
            if (cl == null) {
              cl = ClassLoader.getSystemClassLoader();
            }
            Class treesClass = cl.loadClass("com.sun.source.util.Trees");
            Method instanceMethod = treesClass.getMethod("instance", ProcessingEnvironment.class);
            Method getPathMethod = treesClass.getMethod("getPath", Element.class, AnnotationMirror.class);
            Object trees = instanceMethod.invoke(null, env);
            Object path = getPathMethod.invoke(trees, element, annotation);
            if (path != null) {
              Method getCompilationUnitMethod = path.getClass().getMethod("getCompilationUnit");
              Object cu = getCompilationUnitMethod.invoke(path);
              Method getSourceFileMethod = cu.getClass().getMethod("getSourceFile");
              JavaFileObject file = (JavaFileObject)getSourceFileMethod.invoke(cu);
              URI uri = file.toUri();
              log.info("Resolved uri " + uri + " for package " + context.getPackageName());
              File f = new File(uri.getPath());
              if (f.exists() && f.isFile()) {
                File dir = f.getParentFile().getParentFile();
                javax.lang.model.element.Name name = element.getQualifiedName();
                for (int i = 0;i < name.length();i++) {
                  if (name.charAt(i) == '.') {
                    dir = dir.getParentFile();
                  }
                }
                sourcePathMap.put(context, sourcePath = new DiskFileSystem(dir));
              }
            } else {
              log.info("No path object for package " + context.getPackageName());
            }
          }
          else {
            log.info("Package " + context.getPackageName() + " is not annotated (does not make sense)");
          }
        }
        catch (Exception e) {
          log.info("Could not resolve package " + context, e);
        }
      }
      else {
        log.info("Found cached source path " + sourcePath.getDescription() + " for package " + context.getPackageName());
      }
      return sourcePath;
    }
  }

  /**
   * Resolve a resource from the provided context and path.
   *
   * @param context the context of the application that will help to resolve the path source code
   * @param path the path of the resource to resolve
   * @return the resolved resource or null if it cannot be determined
   * @throws NullPointerException if any argument is null
   * @throws IllegalArgumentException if the context package is not valid
   */
  public FileObject resolveResourceFromSourcePath(ElementHandle.Package context, Path.Absolute path) throws NullPointerException, IllegalArgumentException {
    return resolveResourceFromSourcePath(context, FileKey.newName(path));
  }

  public FileObject resolveResourceFromSourcePath(ElementHandle.Package context, FileKey key) throws NullPointerException, IllegalArgumentException {
    return resolveResourceFromSourcePath(context, Name.parse(key.packageFQN), key.name);
  }

  /**
   * Resolve a resource from the provided context and key.
   *
   * @param context the context of the application that will help to resolve the path source code
   * @param pkg the base package for the resolution
   * @param relativeName the relative name
   * @return the resolved resource or null if it cannot be determined
   * @throws NullPointerException if any argument is null
   * @throws IllegalArgumentException if the context package is not valid
   */
  public FileObject resolveResourceFromSourcePath(
      ElementHandle.Package context,
      Name pkg,
      CharSequence relativeName) throws NullPointerException, IllegalArgumentException {
    if (context == null) {
      throw new NullPointerException("No null package accepted");
    }
    if (pkg == null) {
      throw new NullPointerException("No null pkg accepted");
    }
    if (relativeName == null) {
      throw new NullPointerException("No null relativeName accepted");
    }
    String coordinates = "pkg=" + pkg + " relativeName=" + relativeName;
    ReadFileSystem sourcePath = getSourcePath(context);
    if (sourcePath != null) {
      log.info("Attempt to resolve " + coordinates + " from source path");
      try {
        File f = sourcePath.getPath(pkg);
        if (f != null) {
          f = new File(f, relativeName.toString());
          if (f.exists() && f.isFile()) {
            log.info("Resolved " + coordinates + " to " + f.getAbsolutePath());
            return new JavaFileObjectImpl(StandardLocation.SOURCE_PATH, FileKey.newResourceName(pkg.toString(), relativeName.toString()), sourcePath, f);
          } else {
            log.info("Resolving " + coordinates + " from source path does not exists " + f.getAbsolutePath());
          }
        }
        else {
          log.info("Resolving " + coordinates + " from source path gave no result");
        }
      }
      catch (IOException e) {
        log.info("Could not resolve " + coordinates + " from source path", e);
      }
    }
    else {
      for (StandardLocation location : RESOURCE_LOCATIONS) {
        try {
          log.info("Attempt to resolve " + coordinates + " from " + location.getName());
          FileObject resource = doGetResource(location, pkg, relativeName);
          if (resource != null && resource.getLastModified() > 0) {
            return resource;
          }
        }
        catch (Exception e) {
          log.info("Could not resolve resource " + coordinates + " from " + location.getName(), e);
        }
      }
    }
    return null;
  }

  /**
   * Resolve a resource from the provided context and path.
   *
   * @param context the context of the application that will help to resolve the path source code
   * @param path the path of the resource to resolve
   * @return the resolved resource or null if it cannot be determined
   * @throws NullPointerException if any argument is null
   * @throws IllegalArgumentException if the context package is not valid
   */
  public FileObject resolveResourceFromClassPath(ElementHandle.Package context, Path.Absolute path) throws NullPointerException, IllegalArgumentException {
    return resolveResourceFromClassPath(context, FileKey.newName(path));
  }

  /**
   * Resolve a resource from the provided context and key.
   *
   * @param context the context of the application that will help to resolve the path source code
   * @param key the key of the resource to resolve
   * @return the resolved resource or null if it cannot be determined
   * @throws NullPointerException if any argument is null
   * @throws IllegalArgumentException if the context package is not valid
   */
  public FileObject resolveResourceFromClassPath(ElementHandle.Package context, FileKey key) throws NullPointerException, IllegalArgumentException {
    if (context == null) {
      throw new NullPointerException("No null package accepted");
    }
    if (key == null) {
      throw new NullPointerException("No null path accepted");
    }
    try {
      log.info("Attempt to resolve " + key + " from classpath");
      Filer classPath = env.getFiler();
      FileObject object = classPath.getResource(StandardLocation.CLASS_PATH, key.packageFQN, key.name);
      if (object != null && object.getLastModified() > 0) {
        return object;
      }
    }
    catch (IOException e) {
      log.info("Could not resolve " + key + " from classpath", e);
    }

    //
    return null;
  }

  /**
   * Load a service, this is a replacement method for {@link ServiceLoader#load(Class, ClassLoader)}.
   *
   * @param service the service class to load
   * @param  the service generic type
   * @return an iterable of the loaded services
   * @throws NullPointerException if the service argument is null
   */
  public  Iterable loadServices(Class service) throws NullPointerException {
    if (service == null) {
      throw new NullPointerException("No null service class accepted");
    }
    log.info("Loading services implementation of " + service.getName());
    try {
      return Tools.list(ServiceLoader.load(service, serviceCL));
    }
    catch (ServiceConfigurationError e) {
      log.info("Could not load service for service " + service.getName(), e);
      return Collections.emptyList();
    }
  }

  // Types implementation *********************************************************************************************

  public Element asElement(TypeMirror t) {
    return env.getTypeUtils().asElement(t);
  }

  public boolean isSameType(TypeMirror t1, TypeMirror t2) {
    return env.getTypeUtils().isSameType(t1, t2);
  }

  public boolean isSubtype(TypeMirror t1, TypeMirror t2) {
    return env.getTypeUtils().isSubtype(t1, t2);
  }

  public boolean isAssignable(TypeMirror t1, TypeMirror t2) {
    return env.getTypeUtils().isAssignable(t1, t2);
  }

  public boolean contains(TypeMirror t1, TypeMirror t2) {
    return env.getTypeUtils().contains(t1, t2);
  }

  public boolean isSubsignature(ExecutableType m1, ExecutableType m2) {
    return env.getTypeUtils().isSubsignature(m1, m2);
  }

  public List directSupertypes(TypeMirror t) {
    return env.getTypeUtils().directSupertypes(t);
  }

  public TypeMirror erasure(TypeMirror t) {
    return env.getTypeUtils().erasure(t);
  }

  public TypeElement boxedClass(PrimitiveType p) {
    return env.getTypeUtils().boxedClass(p);
  }

  public PrimitiveType unboxedType(TypeMirror t) {
    return env.getTypeUtils().unboxedType(t);
  }

  public TypeMirror capture(TypeMirror t) {
    return env.getTypeUtils().capture(t);
  }

  public PrimitiveType getPrimitiveType(TypeKind kind) {
    return env.getTypeUtils().getPrimitiveType(kind);
  }

  public NullType getNullType() {
    return env.getTypeUtils().getNullType();
  }

  public NoType getNoType(TypeKind kind) {
    return env.getTypeUtils().getNoType(kind);
  }

  public ArrayType getArrayType(TypeMirror componentType) {
    return env.getTypeUtils().getArrayType(componentType);
  }

  public WildcardType getWildcardType(TypeMirror extendsBound, TypeMirror superBound) {
    return env.getTypeUtils().getWildcardType(extendsBound, superBound);
  }

  public DeclaredType getDeclaredType(TypeElement typeElem, TypeMirror... typeArgs) {
    return env.getTypeUtils().getDeclaredType(typeElem, typeArgs);
  }

  public DeclaredType getDeclaredType(DeclaredType containing, TypeElement typeElem, TypeMirror... typeArgs) {
    return env.getTypeUtils().getDeclaredType(containing, typeElem, typeArgs);
  }

  public TypeMirror asMemberOf(DeclaredType containing, Element element) {
    return env.getTypeUtils().asMemberOf(containing, element);
  }

  public String getLiteralName(TypeMirror typeMirror) {
    TypeMirror erasedParameterTypeMirror = erasure(typeMirror);
    CharSequence typeLiteral = erasedParameterTypeMirror.toString();

    int index = Tools.indexOf(typeLiteral, '<', 0);
    if (index >= 0) {
      // In Eclipse java.util.List generates java.util.List
      // so we must remove this to get a usable literal name
      return typeLiteral.subSequence(0, index).toString();
    } else {
      return typeLiteral.toString();
    }
  }

  // Elements implementation ******************************************************************************************

  public PackageElement getPackageElement(CharSequence name) {
    return env.getElementUtils().getPackageElement(name);
  }

  public TypeElement getTypeElement(CharSequence name) {
    return env.getElementUtils().getTypeElement(name);
  }

  public Map getElementValuesWithDefaults(AnnotationMirror a) {
    return env.getElementUtils().getElementValuesWithDefaults(a);
  }

  public String getDocComment(Element e) {
    return env.getElementUtils().getDocComment(e);
  }

  public boolean isDeprecated(Element e) {
    return env.getElementUtils().isDeprecated(e);
  }

  public javax.lang.model.element.Name getBinaryName(TypeElement type) {
    return env.getElementUtils().getBinaryName(type);
  }

  public PackageElement getPackageOf(Element type) {
    return env.getElementUtils().getPackageOf(type);
  }

  public List getAllMembers(TypeElement type) {
    return env.getElementUtils().getAllMembers(type);
  }

  public List getAllAnnotationMirrors(Element e) {
    return env.getElementUtils().getAllAnnotationMirrors(e);
  }

  public boolean hides(Element hider, Element hidden) {
    return env.getElementUtils().hides(hider, hidden);
  }

  public boolean overrides(ExecutableElement overrider, ExecutableElement overridden, TypeElement type) {
    return env.getElementUtils().overrides(overrider, overridden, type);
  }

  public String getConstantExpression(Object value) {
    return env.getElementUtils().getConstantExpression(value);
  }

  public void printElements(Writer w, Element... elements) {
    env.getElementUtils().printElements(w, elements);
  }

  public javax.lang.model.element.Name getName(CharSequence cs) {
    return env.getElementUtils().getName(cs);
  }

  // Filer implementation *********************************************************************************************

  public JavaFileObject createSourceFile(Name name, Element... originatingElements) throws IOException {
    return createSourceFile((CharSequence)name, originatingElements);
  }

  /**
   * @inheritDoc
   * @throws IllegalArgumentException if the originating element contains a null value
   */
  public JavaFileObject createSourceFile(CharSequence name, Element... originatingElements) throws IOException, IllegalArgumentException {
    // Gard against null element (java compiler is OK but ecj is not)
    for (Object originatingElement : originatingElements) {
      if (originatingElement == null) {
        throw new IllegalArgumentException("Originating elements contains a null element");
      }
    }
    log.info("Creating source file for name=" + name + " elements=" + Arrays.asList(originatingElements));
    return env.getFiler().createSourceFile(name, originatingElements);
  }

  /**
   * @inheritDoc
   * @throws IllegalArgumentException if the originating element contains a null value
   */
  public JavaFileObject createClassFile(CharSequence name, Element... originatingElements) throws IOException, IllegalArgumentException {
    // Gard against null element (java compiler is OK but ecj is not)
    for (Object originatingElement : originatingElements) {
      if (originatingElement == null) {
        throw new IllegalArgumentException("Originating elements contains a null element");
      }
    }
    log.info("Creating class file for name=" + name + " elements=" + Arrays.asList(originatingElements));
    return env.getFiler().createClassFile(name, originatingElements);
  }

  public FileObject createResource(JavaFileManager.Location location, Path.Absolute path, Element... originatingElements) throws IOException {
    return createResource(location, path.getDirs(), path.getSimpleName(), originatingElements);
  }

  public FileObject createResource(JavaFileManager.Location location, FileKey key, Element... originatingElements) throws IOException {
    return createResource(location, key.packageFQN, key.name, originatingElements);
  }

  public FileObject createResource(JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName, Element... originatingElements) throws IOException {
    return createResource(location, Name.parse(pkg), relativeName, originatingElements);
  }

  public FileObject createResource(JavaFileManager.Location location, Name pkg, CharSequence relativeName, Element... originatingElements) throws IOException {
    if (tool.getOverwriteReadingResource()) {
      Key key = new Key(location, pkg, relativeName.toString());
      FileObject resource = resources != null ? resources.get(key) : null;
      if (resource == null) {
        log.info("Creating resource file for location=" + location + " pkg=" + pkg + " relativeName=" + relativeName + " elements=" + Arrays.asList(originatingElements));
        resource = env.getFiler().createResource(location, pkg, relativeName, originatingElements);
        if (resources == null) {
          resources = new HashMap();
        }
        resources.put(new Key(location, pkg, relativeName.toString()), resource);
      }
      return resource;
    } else {
      return env.getFiler().createResource(location, pkg, relativeName, originatingElements);
    }
  }

  public FileObject getResource(JavaFileManager.Location location, Path.Absolute path) throws IOException {
    return getResource(location, path.getDirs(), path.getSimpleName());
  }

  public FileObject getResource(JavaFileManager.Location location, FileKey key) throws IOException {
    return getResource(location, key.packageFQN, key.name);
  }

  public FileObject getResource(JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName) {
    return getResource(location, Name.parse(pkg), relativeName);
  }

  private FileObject doGetResource(JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName) {
    try {
      return env.getFiler().getResource(location, pkg, relativeName);
    }
    catch (IOException e) {
      // Likely to happen in ECJ
      return null;
    }
    catch (RuntimeException e) {
      if (e.getClass().getName().equals("com.sun.tools.javac.util.ClientCodeException") && e.getCause() instanceof NullPointerException) {
        // com.sun.tools.javac.util.ClientCodeException: java.lang.NullPointerException
        // Bug in java compiler >= 1.7 for file not found
        // at com.sun.tools.javac.util.ClientCodeException: java.lang.NullPointerException
        // at com.sun.tools.javac.api.ClientCodeWrapper$WrappedJavaFileManager.getFileForInput(ClientCodeWrapper.java:307)
        // at com.sun.tools.javac.processing.JavacFiler.getResource(JavacFiler.java:472)
        return null;
      } else {
        throw e;
      }
    }
  }

  public FileObject getResource(JavaFileManager.Location location, Name pkg, CharSequence relativeName) {
    Key key = new Key(location, pkg, relativeName.toString());
    String coordinates = "location=" + location + " pkg=" + pkg + " relativeName=" + relativeName;
    FileObject resource = resources != null ? resources.get(key) : null;
    if (resource == null) {
      resource = doGetResource(location, pkg, relativeName);
      if (resource != null) {
        debug("Resolved resolved resource " + coordinates + " from filer");
        if (resource.getLastModified() > 0) {
          if (resources == null) {
            resources = new HashMap();
          }
          debug("Caching locally resolved resource " + coordinates + " from filer");
          resources.put(key, resource);
        } else {
          resource = null;
        }
      } else {
        debug("Could not resolved resource " + coordinates + " from filer");
      }
    }
    return resource;
  }

  public Map getOptions() {
    return env.getOptions();
  }

  // Logger implementation

  @Override
  protected void send(Level level, CharSequence msg, Throwable t) {
    log.log(level, msg, t);
  }

  /**
   * A cache key.
   */
  private static final class Key {
    final JavaFileManager.Location location;
    final Name pkg;
    final String relativeName;
    private Key(JavaFileManager.Location location, Name pkg, String relativeName) {
      this.location = location;
      this.pkg = pkg;
      this.relativeName = relativeName;
    }

    @Override
    public int hashCode() {
      return 13 * (13 * location.hashCode() + pkg.hashCode()) + relativeName.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == this) {
        return true;
      } else if (obj instanceof Key) {
        Key that = (Key)obj;
        return location.equals(that.location) && pkg.equals(that.pkg) && relativeName.equals(that.relativeName);
      } else {
        return false;
      }
    }
  }
}