| /* |
| * Copyright 2008 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 com.google.gwt.resources.ext; |
| |
| import com.google.gwt.core.ext.BadPropertyValueException; |
| import com.google.gwt.core.ext.GeneratorContext; |
| import com.google.gwt.core.ext.PropertyOracle; |
| import com.google.gwt.core.ext.SelectionProperty; |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.core.ext.impl.ResourceGeneratorUtilImpl; |
| import com.google.gwt.core.ext.impl.ResourceLocatorImpl; |
| import com.google.gwt.core.ext.typeinfo.JClassType; |
| import com.google.gwt.core.ext.typeinfo.JMethod; |
| import com.google.gwt.core.ext.typeinfo.JPackage; |
| import com.google.gwt.core.ext.typeinfo.JPrimitiveType; |
| import com.google.gwt.core.ext.typeinfo.JType; |
| import com.google.gwt.core.ext.typeinfo.NotFoundException; |
| import com.google.gwt.dev.resource.ResourceOracle; |
| import com.google.gwt.resources.client.ClientBundle.Source; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.annotation.Annotation; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * Utility methods for building ResourceGenerators. |
| */ |
| public final class ResourceGeneratorUtil { |
| |
| /** |
| * A locator that uses ResourceLocatorImpl to locate resources either in the ResourceOracle or the |
| * ClassLoader. |
| */ |
| private static class GeneralLocator implements Locator { |
| private final ResourceOracle resourceOracle; |
| private final TreeLogger logger; |
| |
| public GeneralLocator(TreeLogger logger, ResourceOracle resourceOracle) { |
| this.logger = logger; |
| this.resourceOracle = resourceOracle; |
| } |
| |
| @Override |
| public URL locate(String resourceName) { |
| return ResourceLocatorImpl.tryFindResourceUrl(logger, resourceOracle, resourceName); |
| } |
| } |
| |
| /** |
| * A locator which will use files published via |
| * {@link ResourceGeneratorUtil#addNamedFile(String, File)}. |
| */ |
| private static class NamedFileLocator implements Locator { |
| public static final NamedFileLocator INSTANCE = new NamedFileLocator(); |
| |
| private NamedFileLocator() { |
| } |
| |
| @Override |
| public URL locate(String resourceName) { |
| File f = ResourceGeneratorUtilImpl.getGeneratedFile(resourceName); |
| if (f != null && f.isFile() && f.canRead()) { |
| try { |
| return f.toURI().toURL(); |
| } catch (MalformedURLException e) { |
| throw new RuntimeException("Unable to make a URL for file " |
| + f.getName()); |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Wrapper interface around different strategies for loading resource data. |
| */ |
| private interface Locator { |
| URL locate(String resourceName); |
| } |
| |
| /** |
| * These are type names from previous APIs or from APIs with similar |
| * functionality that might be confusing. |
| * |
| * @see #checkForDeprecatedAnnotations |
| */ |
| private static final String[] DEPRECATED_ANNOTATION_NAMES = { |
| "com.google.gwt.libideas.resources.client.ImmutableResourceBundle$Resource", |
| "com.google.gwt.user.client.ui.ImageBundle$Resource"}; |
| |
| private static final List<Class<? extends Annotation>> DEPRECATED_ANNOTATION_CLASSES; |
| |
| static { |
| List<Class<? extends Annotation>> classes = new ArrayList<Class<? extends Annotation>>( |
| DEPRECATED_ANNOTATION_NAMES.length); |
| |
| for (String name : DEPRECATED_ANNOTATION_NAMES) { |
| try { |
| Class<?> maybeAnnotation = Class.forName(name, false, |
| ResourceGeneratorUtil.class.getClassLoader()); |
| |
| // Possibly throws ClassCastException |
| Class<? extends Annotation> annotationClass = maybeAnnotation.asSubclass(Annotation.class); |
| |
| classes.add(annotationClass); |
| |
| } catch (ClassCastException e) { |
| // If it's not an Annotation type, we don't care about it |
| } catch (ClassNotFoundException e) { |
| // This is OK; the annotation doesn't exist. |
| } |
| } |
| |
| if (classes.isEmpty()) { |
| DEPRECATED_ANNOTATION_CLASSES = Collections.emptyList(); |
| } else { |
| DEPRECATED_ANNOTATION_CLASSES = Collections.unmodifiableList(classes); |
| } |
| } |
| |
| /** |
| * Publish or override resources named by {@link Source} annotations. This |
| * method is intended to be called by Generators that create ClientBundle |
| * instances and need to pass source data to the ClientBundle system that is |
| * not accessible through the classpath. |
| * |
| * @param resourceName the path at which the contents of <code>file</code> |
| * should be made available |
| * @param file the File whose contents are to be provided to the ClientBundle |
| * system |
| */ |
| // TODO(stalcup): the addNamedFile() and Locator system are redundant and need to be deleted. They |
| // exist because resources generated via the GeneratorContext are not queryable via the available |
| // ResourceOracle. This should be corrected. |
| public static void addNamedFile(String resourceName, File file) { |
| // User code should not refer to private Compiler classes, but it is being done here in lieu of |
| // the above mentioned refactoring. |
| ResourceGeneratorUtilImpl.addGeneratedFile(resourceName, file); |
| } |
| |
| /** |
| * Returns the base filename of a resource. The behavior is similar to the unix |
| * command <code>basename</code>. |
| * |
| * @param resource the URL of the resource |
| * @return the final name segment of the resource |
| */ |
| public static String baseName(URL resource) { |
| String path = resource.getPath(); |
| return path.substring(path.lastIndexOf('/') + 1); |
| } |
| |
| /** |
| * Find all resources referenced by a method in a bundle. The method's |
| * {@link Source} annotation will be examined and the specified locations will |
| * be expanded into URLs by which they may be accessed on the local system. |
| * <p> |
| * This method is sensitive to the <code>locale</code> deferred-binding |
| * property and will attempt to use a best-match lookup by removing locale |
| * components. |
| * <p> |
| * The compiler's ResourceOracle will be used to resolve resource locations. |
| * If the desired resource cannot be found in the ResourceOracle, this method |
| * will fall back to using the current thread's context ClassLoader. If it is |
| * necessary to alter the way in which resources are located, use the overload |
| * that accepts a ClassLoader. |
| * <p> |
| * If the method's return type declares the {@link DefaultExtensions} |
| * annotation, the value of this annotation will be used to find matching |
| * resource names if the method lacks a {@link Source} annotation. |
| * |
| * @param logger a TreeLogger that will be used to report errors or warnings |
| * @param context the ResourceContext in which the ResourceGenerator is |
| * operating |
| * @param method the method to examine for {@link Source} annotations |
| * @return URLs for each {@link Source} annotation value defined on the |
| * method. |
| * @throws UnableToCompleteException if ore or more of the sources could not |
| * be found. The error will be reported via the <code>logger</code> |
| * provided to this method |
| */ |
| public static URL[] findResources(TreeLogger logger, ResourceContext context, |
| JMethod method) throws UnableToCompleteException { |
| JClassType returnType = method.getReturnType().isClassOrInterface(); |
| assert returnType != null; |
| DefaultExtensions annotation = returnType.findAnnotationInTypeHierarchy(DefaultExtensions.class); |
| String[] extensions; |
| if (annotation != null) { |
| extensions = annotation.value(); |
| } else { |
| extensions = new String[0]; |
| } |
| return findResources(logger, context, method, extensions); |
| } |
| |
| /** |
| * Find all resources referenced by a method in a bundle. The method's |
| * {@link Source} annotation will be examined and the specified locations will |
| * be expanded into URLs by which they may be accessed on the local system. |
| * <p> |
| * This method is sensitive to the <code>locale</code> deferred-binding |
| * property and will attempt to use a best-match lookup by removing locale |
| * components. |
| * <p> |
| * The compiler's ResourceOracle will be used to resolve resource locations. |
| * If the desired resource cannot be found in the ResourceOracle, this method |
| * will fall back to using the current thread's context ClassLoader. If it is |
| * necessary to alter the way in which resources are located, use the overload |
| * that accepts a ClassLoader. |
| * |
| * @param logger a TreeLogger that will be used to report errors or warnings |
| * @param context the ResourceContext in which the ResourceGenerator is |
| * operating |
| * @param method the method to examine for {@link Source} annotations |
| * @param defaultSuffixes if the supplied method does not have any |
| * {@link Source} annotations, act as though a Source annotation was |
| * specified, using the name of the method and each of supplied |
| * extensions in the order in which they are specified |
| * @return URLs for each {@link Source} annotation value defined on the |
| * method. |
| * @throws UnableToCompleteException if ore or more of the sources could not |
| * be found. The error will be reported via the <code>logger</code> |
| * provided to this method |
| */ |
| public static URL[] findResources(TreeLogger logger, ResourceContext context, |
| JMethod method, String[] defaultSuffixes) |
| throws UnableToCompleteException { |
| Locator[] locators = getDefaultLocators(logger, context.getGeneratorContext()); |
| URL[] toReturn = findResources(logger, locators, context, method, |
| defaultSuffixes); |
| return toReturn; |
| } |
| |
| /** |
| * Returns the most recent value of the <code>last-modified</code> header fields of all the Urls |
| * in the <code>resources</code> array. |
| * The result is the number of milliseconds since January 1, 1970 GMT. |
| * |
| * Returns 0 if the <code>last-modified</code> header field of one of the resources cannot |
| * be determined. |
| * |
| * @return the most recent modification date of the resources present in |
| * <code>resources</code> or 0 if not known. |
| */ |
| public static long getLastModified(URL[] resources, TreeLogger logger) { |
| long lastModificationDate = 0; |
| for (URL url : resources) { |
| long lastModified = 0; |
| try { |
| lastModified = url.openConnection().getLastModified(); |
| } catch (IOException e) { |
| // Non-fatal, assuming we can re-open the stream later |
| logger.log(TreeLogger.DEBUG, "Could not determine cached time", e); |
| } |
| if (lastModified == 0) { |
| // either we cannot open the stream either the last modification date is not known |
| return 0; |
| } else { |
| lastModificationDate = Math.max(lastModificationDate, lastModified); |
| } |
| } |
| |
| return lastModificationDate; |
| } |
| |
| /** |
| * Finds a method by following a dotted path interpreted as a series of no-arg |
| * method invocations from an instance of a given root type. |
| * |
| * @param rootType the type from which the search begins |
| * @param pathElements a sequence of no-arg method names |
| * @param expectedReturnType the expected return type of the method to locate, |
| * or <code>null</code> if no constraint on the return type is |
| * necessary |
| * |
| * @return the requested JMethod |
| * @throws NotFoundException if the requested method could not be found |
| */ |
| public static JMethod getMethodByPath(JClassType rootType, |
| List<String> pathElements, JType expectedReturnType) |
| throws NotFoundException { |
| if (pathElements.isEmpty()) { |
| throw new NotFoundException("No path specified"); |
| } |
| |
| JMethod currentMethod = null; |
| JType currentType = rootType; |
| for (String pathElement : pathElements) { |
| |
| JClassType referenceType = currentType.isClassOrInterface(); |
| if (referenceType == null) { |
| throw new NotFoundException("Cannot resolve member " + pathElement |
| + " on type " + currentType.getQualifiedSourceName()); |
| } |
| |
| currentMethod = null; |
| searchType : for (JClassType searchType : referenceType.getFlattenedSupertypeHierarchy()) { |
| for (JMethod method : searchType.getOverloads(pathElement)) { |
| if (method.getParameters().length == 0) { |
| currentMethod = method; |
| break searchType; |
| } |
| } |
| } |
| |
| if (currentMethod == null) { |
| throw new NotFoundException("Could not find no-arg method named " |
| + pathElement + " in type " + currentType.getQualifiedSourceName()); |
| } |
| currentType = currentMethod.getReturnType(); |
| } |
| |
| if (expectedReturnType != null) { |
| JPrimitiveType expectedIsPrimitive = expectedReturnType.isPrimitive(); |
| JClassType expectedIsClassType = expectedReturnType.isClassOrInterface(); |
| boolean error = false; |
| |
| if (expectedIsPrimitive != null) { |
| if (!expectedIsPrimitive.equals(currentMethod.getReturnType())) { |
| error = true; |
| } |
| } else { |
| JClassType returnIsClassType = currentMethod.getReturnType().isClassOrInterface(); |
| if (returnIsClassType == null) { |
| error = true; |
| } else if (!expectedIsClassType.isAssignableFrom(returnIsClassType)) { |
| error = true; |
| } |
| } |
| |
| if (error) { |
| throw new NotFoundException("Expecting return type " |
| + expectedReturnType.getQualifiedSourceName() + " found " |
| + currentMethod.getReturnType().getQualifiedSourceName()); |
| } |
| } |
| |
| return currentMethod; |
| } |
| |
| /** |
| * Try to find a resource with the given resourceName. It will use the default |
| * search order to locate the resource as is used by {@link #findResources}. |
| * |
| * @param logger |
| * @param genContext |
| * @param resourceContext |
| * @param resourceName |
| * @return a URL for the resource, if found |
| */ |
| public static URL tryFindResource(TreeLogger logger, |
| GeneratorContext genContext, ResourceContext resourceContext, |
| String resourceName) { |
| String locale = getLocale(logger, genContext); |
| Locator[] locators = getDefaultLocators(logger, genContext); |
| for (Locator locator : locators) { |
| URL toReturn = tryFindResource(locator, resourceContext, resourceName, |
| locale); |
| if (toReturn != null) { |
| return toReturn; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Add the type dependency requirements for a method, to the context. |
| * |
| * @param context |
| * @param method |
| */ |
| private static void addTypeRequirementsForMethod(ResourceContext context, |
| JMethod method) { |
| ClientBundleRequirements reqs = context.getRequirements(); |
| if (reqs != null) { |
| reqs.addTypeHierarchy(method.getEnclosingType()); |
| reqs.addTypeHierarchy((JClassType) method.getReturnType()); |
| } |
| } |
| |
| /** |
| * We want to warn the user about any annotations from ImageBundle or the old |
| * incubator code. |
| */ |
| private static void checkForDeprecatedAnnotations(TreeLogger logger, |
| JMethod method) { |
| |
| for (Class<? extends Annotation> annotationClass : DEPRECATED_ANNOTATION_CLASSES) { |
| if (method.isAnnotationPresent(annotationClass)) { |
| logger.log(TreeLogger.WARN, "Deprecated annotation used; expecting " |
| + Source.class.getCanonicalName() + " but found " |
| + annotationClass.getName() + " instead. It is likely " |
| + "that undesired operation will occur."); |
| } |
| } |
| } |
| |
| /** |
| * Main implementation of findResources. |
| */ |
| private static URL[] findResources(TreeLogger logger, Locator[] locators, |
| ResourceContext context, JMethod method, String[] defaultSuffixes) |
| throws UnableToCompleteException { |
| logger = logger.branch(TreeLogger.DEBUG, "Finding resources"); |
| |
| String locale = getLocale(logger, context.getGeneratorContext()); |
| |
| checkForDeprecatedAnnotations(logger, method); |
| |
| boolean error = false; |
| Source resourceAnnotation = method.getAnnotation(Source.class); |
| URL[] toReturn; |
| |
| if (resourceAnnotation == null) { |
| if (defaultSuffixes != null) { |
| for (String extension : defaultSuffixes) { |
| if (logger.isLoggable(TreeLogger.SPAM)) { |
| logger.log(TreeLogger.SPAM, "Trying default extension " + extension); |
| } |
| for (Locator locator : locators) { |
| URL resourceUrl = tryFindResource(locator, context, |
| getPathRelativeToPackage(method.getEnclosingType().getPackage(), |
| method.getName() + extension), locale); |
| |
| // Take the first match |
| if (resourceUrl != null) { |
| addTypeRequirementsForMethod(context, method); |
| return new URL[] {resourceUrl}; |
| } |
| } |
| } |
| } |
| |
| logger.log(TreeLogger.ERROR, "No " + Source.class.getName() |
| + " annotation and no resources found with default extensions"); |
| toReturn = null; |
| error = true; |
| |
| } else { |
| // The user has put an @Source annotation on the accessor method |
| String[] resources = resourceAnnotation.value(); |
| |
| toReturn = new URL[resources.length]; |
| |
| int tagIndex = 0; |
| for (String resource : resources) { |
| // Try to find the resource relative to the package. |
| URL resourceURL = null; |
| |
| for (Locator locator : locators) { |
| resourceURL = tryFindResource(locator, context, |
| getPathRelativeToPackage(method.getEnclosingType().getPackage(), |
| resource), locale); |
| |
| /* |
| * If we didn't find the resource relative to the package, assume it |
| * is absolute. |
| */ |
| if (resourceURL == null) { |
| resourceURL = tryFindResource(locator, context, resource, locale); |
| } |
| |
| // If we have found a resource, take the first match |
| if (resourceURL != null) { |
| break; |
| } |
| } |
| |
| if (resourceURL == null) { |
| error = true; |
| logger.log(TreeLogger.ERROR, "Resource " + resource |
| + " not found. Is the name specified as ClassLoader.getResource()" |
| + " would expect?"); |
| } |
| |
| toReturn[tagIndex++] = resourceURL; |
| } |
| } |
| |
| if (error) { |
| throw new UnableToCompleteException(); |
| } |
| |
| addTypeRequirementsForMethod(context, method); |
| return toReturn; |
| } |
| |
| /** |
| * Get default list of resource Locators, in the default order. |
| * |
| * @return an ordered array of Locator[] |
| */ |
| private static Locator[] getDefaultLocators(TreeLogger logger, GeneratorContext genContext) { |
| return new Locator[] { |
| NamedFileLocator.INSTANCE, new GeneralLocator(logger, genContext.getResourcesOracle())}; |
| } |
| |
| /** |
| * Get the current locale string. |
| * |
| * @param logger |
| * @param genContext |
| * @return the current locale |
| */ |
| private static String getLocale(TreeLogger logger, GeneratorContext genContext) { |
| String locale; |
| try { |
| PropertyOracle oracle = genContext.getPropertyOracle(); |
| SelectionProperty prop = oracle.getSelectionProperty(logger, "locale"); |
| locale = prop.getCurrentValue(); |
| } catch (BadPropertyValueException e) { |
| locale = null; |
| } |
| return locale; |
| } |
| |
| /** |
| * Converts a package relative path into an absolute path. |
| * |
| * @param pkg the package |
| * @param path a path relative to the package |
| * @return an absolute path |
| */ |
| private static String getPathRelativeToPackage(JPackage pkg, String path) { |
| return pkg.getName().replace('.', '/') + '/' + path; |
| } |
| |
| /** |
| * This performs the locale lookup function for a given resource name. |
| * |
| * @param locator the Locator to use to load the resources |
| * @param resourceName the string name of the desired resource |
| * @param locale the locale of the current rebind permutation |
| * @return a URL by which the resource can be loaded, <code>null</code> if one |
| * cannot be found |
| */ |
| private static URL tryFindResource(Locator locator, String resourceName, |
| String locale) { |
| URL toReturn = null; |
| |
| // Look for locale-specific variants of individual resources |
| if (locale != null) { |
| // Convert language_country_variant to independent pieces |
| String[] localeSegments = locale.split("_"); |
| int lastDot = resourceName.lastIndexOf("."); |
| String prefix = lastDot == -1 ? resourceName : resourceName.substring(0, |
| lastDot); |
| String extension = lastDot == -1 ? "" : resourceName.substring(lastDot); |
| |
| for (int i = localeSegments.length - 1; i >= -1; i--) { |
| String localeInsert = ""; |
| for (int j = 0; j <= i; j++) { |
| localeInsert += "_" + localeSegments[j]; |
| } |
| |
| toReturn = locator.locate(prefix + localeInsert + extension); |
| if (toReturn != null) { |
| break; |
| } |
| } |
| } else { |
| toReturn = locator.locate(resourceName); |
| } |
| |
| return toReturn; |
| } |
| |
| /** |
| * Performs the locale lookup function for a given resource name. Will also |
| * add the located resource to the requirements object for the context. |
| * |
| * @param locator the Locator to use to load the resources |
| * @param context the ResourceContext |
| * @param resourceName the string name of the desired resource |
| * @param locale the locale of the current rebind permutation |
| * @return a URL by which the resource can be loaded, <code>null</code> if one |
| * cannot be found |
| */ |
| private static URL tryFindResource(Locator locator, ResourceContext context, |
| String resourceName, String locale) { |
| |
| URL toReturn = tryFindResource(locator, resourceName, locale); |
| if (context != null) { |
| ClientBundleRequirements reqs = context.getRequirements(); |
| if (reqs != null) { |
| reqs.addResolvedResource(resourceName, toReturn); |
| } |
| } |
| |
| return toReturn; |
| } |
| |
| /** |
| * Utility class. |
| */ |
| private ResourceGeneratorUtil() { |
| } |
| } |