This allows multiple source files to be specified for a ui:style, just
like any other CssResource.

It also allows you to put body text on a ui:style that has
sources. The body becomes just one more source file, the last
one--nice for local tweaks on centrally defined styles.

ResourceGeneratorUtil patch by bobv, reviewed by rjrjr
Remainder by rjrjr, reviewed by jgw

http://gwt-code-reviews.appspot.com/77819

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6479 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java b/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
index d2efb20..c0ea076 100644
--- a/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
+++ b/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
@@ -202,8 +202,8 @@
   public static URL[] findResources(TreeLogger logger, ClassLoader classLoader,
       ResourceContext context, JMethod method, String[] defaultSuffixes)
       throws UnableToCompleteException {
-    return findResources(logger, new ClassLoaderLocator(classLoader), context,
-        method, defaultSuffixes, true);
+    return findResources(logger, new Locator[] {new ClassLoaderLocator(
+        classLoader)}, context, method, defaultSuffixes);
   }
 
   /**
@@ -281,34 +281,14 @@
   public static URL[] findResources(TreeLogger logger, ResourceContext context,
       JMethod method, String[] defaultSuffixes)
       throws UnableToCompleteException {
-    URL[] toReturn = null;
-    Locator locator;
+    Locator[] locators = {
+        FileLocator.INSTANCE,
+        new ResourceOracleLocator(
+            context.getGeneratorContext().getResourcesOracle()),
+        new ClassLoaderLocator(Thread.currentThread().getContextClassLoader())};
 
-    // If we have named files, attempt them first
-    if (!namedFiles.isEmpty()) {
-      toReturn = findResources(logger, FileLocator.INSTANCE, context, method,
-          defaultSuffixes, false);
-    }
-
-    if (toReturn == null) {
-      // Try to find the resources with ResourceOracle
-      locator = new ResourceOracleLocator(
-          context.getGeneratorContext().getResourcesOracle());
-
-      // Don't report errors since we have a fallback mechanism
-      toReturn = findResources(logger, locator, context, method,
-          defaultSuffixes, false);
-    }
-
-    if (toReturn == null) {
-      // Since not all resources were found, try with ClassLoader
-      locator = new ClassLoaderLocator(
-          Thread.currentThread().getContextClassLoader());
-
-      // Do report hard failures
-      toReturn = findResources(logger, locator, context, method,
-          defaultSuffixes, true);
-    }
+    URL[] toReturn = findResources(logger, locators, context, method,
+        defaultSuffixes);
     return toReturn;
   }
 
@@ -331,14 +311,10 @@
 
   /**
    * Main implementation of findResources.
-   * 
-   * @param reportErrors controls whether or not the inability to locate any
-   *          given resource should be a hard error (throw an UnableToComplete)
-   *          or a soft error (return null).
    */
-  private static URL[] findResources(TreeLogger logger, Locator locator,
-      ResourceContext context, JMethod method, String[] defaultSuffixes,
-      boolean reportErrors) throws UnableToCompleteException {
+  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;
@@ -360,21 +336,22 @@
       if (defaultSuffixes != null) {
         for (String extension : defaultSuffixes) {
           logger.log(TreeLogger.SPAM, "Trying default extension " + extension);
-          URL resourceUrl = tryFindResource(locator, getPathRelativeToPackage(
-              method.getEnclosingType().getPackage(), method.getName()
-                  + extension), locale);
+          for (Locator locator : locators) {
+            URL resourceUrl = tryFindResource(locator,
+                getPathRelativeToPackage(
+                    method.getEnclosingType().getPackage(), method.getName()
+                        + extension), locale);
 
-          if (resourceUrl != null) {
-            // Early out because we found a hit
-            return new URL[] {resourceUrl};
+            if (resourceUrl != null) {
+              // Take the first match
+              return new URL[] {resourceUrl};
+            }
           }
         }
       }
 
-      if (reportErrors) {
-        logger.log(TreeLogger.ERROR, "No " + Source.class.getName()
-            + " annotation and no resources found with default extensions");
-      }
+      logger.log(TreeLogger.ERROR, "No " + Source.class.getName()
+          + " annotation and no resources found with default extensions");
       toReturn = null;
       error = true;
 
@@ -387,26 +364,31 @@
       int tagIndex = 0;
       for (String resource : resources) {
         // Try to find the resource relative to the package.
-        URL resourceURL = tryFindResource(locator, getPathRelativeToPackage(
-            method.getEnclosingType().getPackage(), resource), locale);
+        URL resourceURL = null;
 
-        // If we didn't find the resource relative to the package, assume it is
-        // absolute.
-        if (resourceURL == null) {
-          resourceURL = tryFindResource(locator, resource, locale);
+        for (Locator locator : locators) {
+          resourceURL = tryFindResource(locator, 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, resource, locale);
+          }
+
+          // If we have found a resource, take the first match
+          if (resourceURL != null) {
+            break;
+          }
         }
 
         if (resourceURL == null) {
           error = true;
-          if (reportErrors) {
-            logger.log(TreeLogger.ERROR, "Resource " + resource
-                + " not found. Is the name specified as Class.getResource()"
-                + " would expect?");
-          } else {
-            // Speculative attempts should not emit errors
-            logger.log(TreeLogger.DEBUG, "Stopping because " + resource
-                + " not found");
-          }
+          logger.log(TreeLogger.ERROR, "Resource " + resource
+              + " not found. Is the name specified as Class.getResource()"
+              + " would expect?");
         }
 
         toReturn[tagIndex++] = resourceURL;
@@ -414,11 +396,7 @@
     }
 
     if (error) {
-      if (reportErrors) {
-        throw new UnableToCompleteException();
-      } else {
-        return null;
-      }
+      throw new UnableToCompleteException();
     }
 
     return toReturn;
diff --git a/user/src/com/google/gwt/uibinder/rebind/BundleWriter.java b/user/src/com/google/gwt/uibinder/rebind/BundleWriter.java
index 1db5074..b0a00b4 100644
--- a/user/src/com/google/gwt/uibinder/rebind/BundleWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/BundleWriter.java
@@ -29,6 +29,7 @@
 import com.google.gwt.uibinder.rebind.model.ImplicitDataResource;
 import com.google.gwt.uibinder.rebind.model.ImplicitImageResource;
 
+import java.util.Collection;
 import java.util.Set;
 
 /**
@@ -96,7 +97,7 @@
     
     // Write css methods
     for (ImplicitCssResource css : bundleClass.getCssMethods()) {
-      writer.write("@Source(\"%s\")", css.getSource());
+      writeCssSource(css);
       writeCssImports(css);
       writer.write("%s %s();", css.getClassName(), css.getName());
       writer.newline();
@@ -136,6 +137,22 @@
     }
   }
 
+  private void writeCssSource(ImplicitCssResource css) {
+    Collection<String> sources = css.getSource();
+    if (sources.size() == 1) {
+      writer.write("@Source(\"%s\")", sources.iterator().next());
+    } else {
+      StringBuffer b = new StringBuffer();
+      for (String s : sources) {
+        if (b.length() > 0) {
+          b.append(", ");
+        }
+        b.append('"').append(s).append('"');
+      }
+      writer.write("@Source({%s})", b);
+    }
+  }
+
   private void writeImageMethods() {
     for (ImplicitImageResource image : bundleClass.getImageMethods()) {
       if (null != image.getSource()) {
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
index d6dca10..26579e3 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
@@ -224,13 +224,12 @@
 
   private void createStyle(XMLElement elem) throws UnableToCompleteException {
     String body = elem.consumeUnescapedInnerText();
-    if (body.length() > 0 && elem.hasAttribute(SOURCE_ATTRIBUTE)) {
-      writer.die(
-          "In %s, cannot use both a source attribute and inline css text.",
-          elem);
-    }
-
     String source = elem.consumeAttribute(SOURCE_ATTRIBUTE);
+    
+    if (0 == body.length() && 0 == source.length()) {
+      writer.die("%s must have either a src attribute or body text", elem);
+    }
+    
     String name = elem.consumeAttribute(FIELD_ATTRIBUTE, "style");
     JClassType publicType = consumeCssResourceType(elem);
 
diff --git a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
index f538c3f..6d59267 100644
--- a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
+++ b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
@@ -118,7 +118,7 @@
    * was unset. The returned string is not escaped.
    * 
    * @param name the attribute's full name (including prefix)
-   * @return the attribute's value, or null
+   * @return the attribute's value, or ""
    */
   public String consumeAttribute(String name) {
     String value = elem.getAttribute(name);
diff --git a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
index b5c5bb6..588921e 100644
--- a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
+++ b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
@@ -29,22 +29,34 @@
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
  * Models a method returning a CssResource on a generated ClientBundle.
  */
 public class ImplicitCssResource {
+  private static List<String> explodeSource(String source) {
+    if (source.length() == 0) {
+      return Collections.emptyList();
+    }
+    return Arrays.asList(source.split("\\s+"));
+  }
+
   private final String packageName;
   private final String className;
   private final String name;
-  private final String source;
+  private final List<String> sources;
   private final JClassType extendedInterface;
   private final String body;
   private final MortalLogger logger;
   private final Set<JClassType> imports;
+
   private File generatedFile;
 
   ImplicitCssResource(String packageName, String className, String name,
@@ -57,20 +69,7 @@
     this.body = body;
     this.logger = logger;
     this.imports = Collections.unmodifiableSet(importTypes);
-
-    if (body.length() > 0) {
-      assert "".equals(source); // Enforced for real by the parser
-
-      /*
-       * We're going to write the inline body to a temporary File and register
-       * it with the CssResource world under the name in this.source, via
-       * ResourceGeneratorUtil.addNamedFile(). When CssResourceGenerator sees
-       * this name in an @Source annotation it will know to use the registered
-       * file rather than load a resource.
-       */
-      source = String.format("uibinder:%s.%s.css", packageName, className);
-    }
-    this.source = source;
+    sources = explodeSource(source);
   }
 
   /**
@@ -87,20 +86,22 @@
    *           can't find.
    */
   public Set<String> getCssClassNames() throws UnableToCompleteException {
-    URL[] urls;
+    List<URL> urls = getExternalCss();
 
-    if (body.length() == 0) {
-      urls = getExternalCss();
-    } else {
+    final File bodyFile = getGeneratedFile();
+    if (bodyFile != null) {
       try {
-        urls = new URL[] {getGeneratedFile().toURI().toURL()};
+        urls.add(bodyFile.toURI().toURL());
       } catch (MalformedURLException e) {
         throw new RuntimeException(e);
       }
     }
+    assert urls.size() > 0;
 
-    CssStylesheet sheet = GenerateCssAst.exec(logger.getTreeLogger(), urls);
-    return ExtractClassNamesVisitor.exec(sheet, imports.toArray(new JClassType[imports.size()]));
+    CssStylesheet sheet = GenerateCssAst.exec(logger.getTreeLogger(),
+        urls.toArray(new URL[urls.size()]));
+    return ExtractClassNamesVisitor.exec(sheet,
+        imports.toArray(new JClassType[imports.size()]));
   }
 
   /**
@@ -133,11 +134,21 @@
   /**
    * @return the name of the .css file(s), separate by white space
    */
-  public String getSource() {
-    return source;
+  public Collection<String> getSource() {
+    if (body.length() == 0) {
+      return Collections.unmodifiableCollection(sources);
+    }
+
+    List<String> rtn = new ArrayList<String>(sources);
+    rtn.add(getBodyFileName());
+    return rtn;
   }
 
-  private URL[] getExternalCss() throws UnableToCompleteException {
+  private String getBodyFileName() {
+    return String.format("uibinder:%s.%s.css", packageName, className);
+  }
+
+  private List<URL> getExternalCss() throws UnableToCompleteException {
     /*
      * TODO(rjrjr,bobv) refactor ResourceGeneratorUtil.findResources so we can
      * find them the same way ClientBundle does. For now, just look relative to
@@ -147,9 +158,7 @@
     ClassLoader classLoader = ImplicitCssResource.class.getClassLoader();
     String path = packageName.replace(".", "/");
 
-    String[] sources = source.split(" ");
-    URL[] urls = new URL[sources.length];
-    int i = 0;
+    List<URL> urls = new ArrayList<URL>();
 
     for (String s : sources) {
       String resourcePath = path + '/' + s;
@@ -157,12 +166,16 @@
       if (null == found) {
         logger.die("Unable to find resource: " + resourcePath);
       }
-      urls[i++] = found;
+      urls.add(found);
     }
     return urls;
   }
 
   private File getGeneratedFile() {
+    if (body.length() == 0) {
+      return null;
+    }
+
     if (generatedFile == null) {
       try {
         File f = File.createTempFile(String.format("uiBinder_%s_%s",
@@ -175,7 +188,7 @@
       } catch (IOException e) {
         throw new RuntimeException(e);
       }
-      ResourceGeneratorUtil.addNamedFile(getSource(), generatedFile);
+      ResourceGeneratorUtil.addNamedFile(getBodyFileName(), generatedFile);
     }
     return generatedFile;
   }
diff --git a/user/src/com/google/gwt/uibinder/sample/client/Menu.css b/user/src/com/google/gwt/uibinder/sample/client/Menu.css
new file mode 100644
index 0000000..9bd75be
--- /dev/null
+++ b/user/src/com/google/gwt/uibinder/sample/client/Menu.css
@@ -0,0 +1,4 @@
+.menuBar {
+  border:solid;
+  background-color:white;
+}
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
index c712178..6ef8738 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
@@ -1,8 +1,3 @@
-.menuBar {
-  border:solid;
-  background-color:white;
-}
-
 /*
  * Demonstrates that the ui.xml has access to styles that 
  * do not back any declared CssResource api
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
index d8ffecd..b828a9c 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
@@ -71,9 +71,13 @@
 <!-- 
   Tests creating a CssResource from an external file.
  -->
-<ui:style field='myStyle' src='WidgetBasedUi.css' 
-    type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'/>
-
+<ui:style field='myStyle' src='WidgetBasedUi.css Menu.css' 
+    type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'>
+    .menuBar { 
+      font-family: sans-serif;
+    }
+</ui:style>
+ 
 <ui:style field='myOtherStyle' type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'>
   /* because we extend WidgetBasedUi.Style and it's tagged @Shared, we can refine the menubar 
      style defined in other files */
diff --git a/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java b/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
index 9528470..7a12ded 100644
--- a/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
+++ b/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
@@ -231,7 +231,7 @@
   }
 
   interface Bundle extends ClientBundle {
-    @Source("WidgetBasedUi.css")
+    @Source({"WidgetBasedUi.css", "Menu.css"})
     @NotStrict
     WidgetBasedUi.Style style();
   }