Introduces inline styles to ui.xml files, completing
http://code.google.com/p/google-web-toolkit/issues/detail?id=3984 to a
first approximation.

Only CssResource works so far, via <ui:style>. @Sprite won't work
until <ui:image> is implemented, and <ui:data> is still needed as well.

Reviewed by: spoon


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6114 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
index a6fcec7..f26d9ca 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
@@ -20,6 +20,7 @@
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.parsers.NullInterpreter;
 import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
 import com.google.gwt.uibinder.rebind.model.ImplicitClientBundle;
 import com.google.gwt.uibinder.rebind.model.ImplicitCssResource;
@@ -39,6 +40,7 @@
   private final MessagesWriter messagesWriter;
   private final FieldManager fieldManager;
   private final ImplicitClientBundle bundleClass;
+  private final JClassType cssResourceType;
 
   public UiBinderParser(UiBinderWriter writer, MessagesWriter messagesWriter,
       FieldManager fieldManager, TypeOracle oracle,
@@ -48,7 +50,8 @@
     this.messagesWriter = messagesWriter;
     this.fieldManager = fieldManager;
     this.bundleClass = bundleClass;
-  }
+    this.cssResourceType = oracle.findType(CssResource.class.getCanonicalName());
+ }
 
   /**
    * Parses the root UiBinder element, and kicks off the parsing of the rest of
@@ -56,7 +59,7 @@
    */
   public String parse(XMLElement elem) throws UnableToCompleteException {
     // TODO(rjrjr) Clearly need to break these find* methods out into their own
-    // parsers, need registration scheme for ui binder's own parsers
+    // parsers, an so need a registration scheme for uibinder-specific parsers
     findStyles(elem);
     findResources(elem);
     messagesWriter.findMessagesConfig(elem);
@@ -66,8 +69,16 @@
 
   private JClassType consumeCssResourceType(XMLElement elem)
       throws UnableToCompleteException {
-    JClassType publicType = consumeTypeAttribute(elem);
-    JClassType cssResourceType = oracle.findType(CssResource.class.getCanonicalName());
+    String typeName = elem.consumeAttribute("type", null);
+    if (typeName == null) {
+      return cssResourceType;
+    }
+
+    JClassType publicType = oracle.findType(typeName);
+    if (publicType == null) {
+      writer.die("In %s, no such type %s", elem, typeName);
+    }
+
     if (!publicType.isAssignableTo(cssResourceType)) {
       writer.die("In %s, type %s does not extend %s", elem,
           publicType.getQualifiedSourceName(),
@@ -131,13 +142,18 @@
   }
 
   private void createStyle(XMLElement elem) throws UnableToCompleteException {
-    // Won't be required for long
-    String source = elem.consumeRequiredAttribute("source");
+    String body =  elem.consumeInnerText(new NullInterpreter<String>());
+    if (body.length() > 0 && elem.hasAttribute("source")) {
+      writer.die("In %s, cannot use both a source attribute and inline css text.", elem);
+    }
+
+    String source = elem.consumeAttribute("source");
     String name = elem.consumeAttribute("field", "style");
     JClassType publicType = consumeCssResourceType(elem);
 
     ImplicitCssResource cssMethod = bundleClass.createCssResource(name, source,
-        publicType);
+        publicType, body);
+
     FieldWriter field = fieldManager.registerFieldOfGeneratedType(
         cssMethod.getPackageName(), cssMethod.getClassName(),
         cssMethod.getName());
diff --git a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
index 2f6d347..0ff9000 100644
--- a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
+++ b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -47,7 +47,7 @@
   public interface Interpreter<T> {
     /**
      * Given an XMLElement, return its filtered value.
-     * 
+     *
      * @throws UnableToCompleteException on error
      */
     T interpretElement(XMLElement elem) throws UnableToCompleteException;
@@ -115,7 +115,7 @@
   /**
    * Consumes the given attribute and returns its trimmed value, or null if it
    * was unset. The returned string is not escaped.
-   * 
+   *
    * @param name the attribute's full name (including prefix)
    * @return the attribute's value, or null
    */
@@ -128,7 +128,7 @@
   /**
    * Consumes the given attribute and returns its trimmed value, or the given
    * default value if it was unset. The returned string is not escaped.
-   * 
+   *
    * @param name the attribute's full name (including prefix)
    * @param defaultValue the value to return if the attribute was unset
    * @return the attribute's value, or defaultValue
@@ -143,7 +143,7 @@
 
   /**
    * Consumes the given attribute as a boolean value.
-   * 
+   *
    * @throws UnableToCompleteException
    */
   public boolean consumeBooleanAttribute(String attr)
@@ -177,9 +177,9 @@
    * Consumes and returns all child elements selected by the interpreter. Note
    * that text nodes are not elements, and so are not presented for
    * interpretation, and are not consumed.
-   * 
+   *
    * @param interpreter Should return true for any child that should be consumed
-   *          and returned.
+   *          and returned by the consumeChildElements call
    * @throws UnableToCompleteException
    */
   public Collection<XMLElement> consumeChildElements(
@@ -217,7 +217,7 @@
    * The odds are you want to use
    * {@link com.google.gwt.templates.parsers.HtmlInterpreter} for an HTML value,
    * or {@link com.google.gwt.templates.parsers.TextInterpreter} for text.
-   * 
+   *
    * @param interpreter Called for each element, expected to return a string
    *          replacement for it, or null if it should be left as is
    */
@@ -250,7 +250,7 @@
    * This call requires an interpreter to make sense of any special children.
    * The odds are you want to use
    * {@link com.google.gwt.templates.parsers.TextInterpreter}
-   * 
+   *
    * @throws UnableToCompleteException If any elements present are not consumed
    *           by the interpreter
    */
@@ -292,7 +292,7 @@
    */
   public String consumeOpeningTag() {
     String rtn = getOpeningTag();
-  
+
     for (int i = getAttributeCount() - 1; i >= 0; i--) {
       getAttribute(i).consumeValue();
     }
@@ -306,7 +306,7 @@
       throws UnableToCompleteException {
     String value = consumeAttribute(name);
     if ("".equals(value)) {
-      writer.die("In %s, missing required attribute name\"%s\"", this);
+      writer.die("In %s, missing required attribute name\"%s\"", this, name);
     }
     return value;
   }
@@ -319,8 +319,9 @@
     XMLElement ret = null;
     for (XMLElement child : consumeChildElements()) {
       if (ret != null) {
-        throw new RuntimeException(getLocalName()
-            + " may only contain a single child element.");
+        throw new RuntimeException(String.format(
+            "%s may only contain a single child element, but found"
+                + "%s and %s.", getLocalName(), ret, child));
       }
 
       ret = child;
diff --git a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java
index 3c1457e..3f17973 100644
--- a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java
+++ b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java
@@ -44,8 +44,8 @@
   public ImplicitClientBundle(String packageName, String uiBinderImplClassName,
       String fieldName, MortalLogger logger) {
     this.packageName = packageName;
-    this.className = uiBinderImplClassName + "GenBundle";
-    this.cssBaseName = uiBinderImplClassName + "GenCss";
+    this.className = uiBinderImplClassName + "_GenBundle";
+    this.cssBaseName = uiBinderImplClassName + "_GenCss";
     this.fieldName = fieldName;
     this.logger = logger;
   }
@@ -57,12 +57,13 @@
    * @param source path to the .css file resource
    * @param extendedInterface the public interface implemented by this
    *          CssResource, or null
+   * @param body the inline css text
    * @return
    */
   public ImplicitCssResource createCssResource(String name, String source,
-      JClassType extendedInterface) {
+      JClassType extendedInterface, String body) {
     ImplicitCssResource css = new ImplicitCssResource(packageName, cssBaseName
-        + name, name, source, extendedInterface, logger);
+        + name, name, source, extendedInterface, body, logger);
     cssMethods.add(css);
     return css;
   }
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 76284da..5742cec 100644
--- a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
+++ b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
@@ -20,8 +20,14 @@
 import com.google.gwt.resources.css.ExtractClassNamesVisitor;
 import com.google.gwt.resources.css.GenerateCssAst;
 import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
 import com.google.gwt.uibinder.rebind.MortalLogger;
 
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Set;
 
@@ -36,16 +42,33 @@
   private final String name;
   private final String source;
   private final JClassType extendedInterface;
+  private final String body;
   private final MortalLogger logger;
+  private File generatedFile;
 
   public ImplicitCssResource(String packageName, String className, String name,
-      String source, JClassType extendedInterface, MortalLogger logger) {
+      String source, JClassType extendedInterface, String body,
+      MortalLogger logger) {
     this.packageName = packageName;
     this.className = className;
     this.name = name;
-    this.source = source;
     this.extendedInterface = extendedInterface;
+    this.body = body;
     this.logger = logger;
+
+    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;
   }
 
   /**
@@ -58,29 +81,20 @@
   /**
    * @return the set of CSS classnames in the underlying .css files
    *
-   * @throws UnableToCompleteException if the user has called for a .css file we can't find.
+   * @throws UnableToCompleteException if the user has called for a .css file we
+   *           can't find.
    */
   public Set<String> getCssClassNames() throws UnableToCompleteException {
-    /*
-     * TODO(rjrjr,bobv) refactor ResourceGeneratorUtil.findResources so we can
-     * find them the same way ClientBundle does. For now, just look relative to
-     * this package
-     */
+    URL[] urls;
 
-    ClassLoader classLoader = ImplicitCssResource.class.getClassLoader();
-    String path = packageName.replace(".", "/");
-
-    String[] sources = source.split(" ");
-    URL[] urls = new URL[sources.length];
-    int i = 0;
-
-    for (String s : sources) {
-      String resourcePath = path + '/' + s;
-      URL found = classLoader.getResource(resourcePath);
-      if (null == found) {
-        logger.die("Unable to find resource: " + resourcePath);
+    if (body.length() == 0) {
+      urls = getExternalCss();
+    } else {
+      try {
+        urls = new URL[] {getGeneratedFile().toURL()};
+      } catch (MalformedURLException e) {
+        throw new RuntimeException(e);
       }
-      urls[i++] = found;
     }
 
     CssStylesheet sheet = GenerateCssAst.exec(logger.getTreeLogger(), urls);
@@ -111,9 +125,52 @@
   }
 
   /**
-   * @return the user declared names of the associated .css files
+   * @return the name of the .css file(s), separate by white space
    */
   public String getSource() {
     return source;
   }
+
+  private 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
+     * this package
+     */
+
+    ClassLoader classLoader = ImplicitCssResource.class.getClassLoader();
+    String path = packageName.replace(".", "/");
+
+    String[] sources = source.split(" ");
+    URL[] urls = new URL[sources.length];
+    int i = 0;
+
+    for (String s : sources) {
+      String resourcePath = path + '/' + s;
+      URL found = classLoader.getResource(resourcePath);
+      if (null == found) {
+        logger.die("Unable to find resource: " + resourcePath);
+      }
+      urls[i++] = found;
+    }
+    return urls;
+  }
+
+  private File getGeneratedFile() {
+    if (generatedFile == null) {
+      try {
+        File f = File.createTempFile(String.format("uiBinder_%s_%s",
+            packageName, className), ".css");
+
+        BufferedWriter out = new BufferedWriter(new FileWriter(f));
+        out.write(body);
+        out.close();
+        generatedFile = f;
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+      ResourceGeneratorUtil.addNamedFile(getSource(), generatedFile);
+    }
+    return generatedFile;
+  }
 }
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 3d0f78d..c712178 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
@@ -7,6 +7,6 @@
  * Demonstrates that the ui.xml has access to styles that 
  * do not back any declared CssResource api
  */
-.privateStyle {
+.privateColor {
   color: SteelBlue;
 }
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
index bd47e88..edb568f 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
@@ -111,6 +111,8 @@
   @UiField DListElement widgetCrazyDefinitionList;
   @UiField HTMLPanel customTagHtmlPanel;
   @UiField ParagraphElement privateStyleParagraph;
+  @UiField ParagraphElement reallyPrivateStyleParagraph;
+  @UiField SpanElement totallyPrivateStyleSpan;
 
   public WidgetBasedUi() {
     this.bundledLabel = new Label();
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 288690d..33ba429 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
@@ -69,7 +69,30 @@
 <!-- 
   Tests creating a CssResource from an external file.
  -->
-<ui:style field='myStyle' source='WidgetBasedUi.css' type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'/>
+<ui:style field='myStyle' source='WidgetBasedUi.css' 
+    type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.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 */
+  
+  .menuBar.psychedelic {
+    background-color: Yellow;
+  }
+
+  /* Visible only to this template  */
+  .privateColor {
+    color: Purple;
+  }
+</ui:style>
+
+<ui:style field='myTotallyPrivateStyle'>
+  /* An inline style tied to no public type */
+  
+  .superPrivateColor {
+    background-color: Gold;
+  }
+</ui:style>
 
 <gwt:DockPanel ui:field="root" width="100%">
   <gwt:Dock direction='NORTH'>
@@ -149,7 +172,7 @@
                 Piggledy
               </ui:msg>
               </div>
-            <gwt:MenuBar vertical="true" styleName="{myStyle.menuBar}">
+            <gwt:MenuBar vertical="true" styleName="{myStyle.menuBar} {myOtherStyle.psychedelic}">
               <gwt:MenuItem>delta</gwt:MenuItem>
               <gwt:MenuItem>echo</gwt:MenuItem>
               <gwt:MenuItem>foxtrot</gwt:MenuItem>
@@ -264,8 +287,14 @@
         This stylish paragraph also gets its good looks from the
         external ClientBundle.
       </p>
-      <p ui:field='privateStyleParagraph' class='{myStyle.privateStyle}'>
-        This one is has a private style, visible only inside the ui.xml file.
+      <p ui:field='privateStyleParagraph' class='{myStyle.privateColor}'>
+        This one is has a private style, defined out in WidgetBaseUi.css and used only by this ui.xml file.
+      </p>
+      <p ui:field='reallyPrivateStyleParagraph' class='{myOtherStyle.privateColor}'>
+        And this style is defined right here inside the ui.xml file. 
+        <span ui:field='totallyPrivateStyleSpan' class='{myTotallyPrivateStyle.superPrivateColor}'>
+          (This one too, but even more so.)
+        </span>
       </p>
       <h2>Evolving</h2>
       <p>
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 5172b0c..e2ba40c 100644
--- a/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
+++ b/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
@@ -265,11 +265,22 @@
     assertTrue(((HTML) north).getHTML().contains("Title area"));
   }
 
-  public void testPrivateStyle() {
+  public void testPrivateStyleFromExternalCss() {
     ParagraphElement p = widgetUi.privateStyleParagraph;
     assertTrue("Some kind of class should be set", p.getClassName().length() > 0);
   }
 
+  public void testPrivateStylesFromInlineCss() {
+    ParagraphElement p = widgetUi.reallyPrivateStyleParagraph;
+    assertTrue("Some kind of class should be set",
+        p.getClassName().length() > 0);
+    assertFalse("Should be a different style than privateStyleParagraph's",
+        widgetUi.privateStyleParagraph.getClassName().equals(p.getClassName()));
+
+    assertTrue("Some kind of class should be set",
+        widgetUi.totallyPrivateStyleSpan.getClassName().length() > 0);
+  }
+
   @DoNotRunWith(Platform.Htmlunit)
   public void testRadioButton() {
     RadioButton able = widgetUi.myRadioAble;