View Javadoc

1   /*
2    * Copyright 2008 Google Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  package com.google.gwt.uibinder.rebind;
17  
18  import com.google.gwt.core.ext.UnableToCompleteException;
19  import com.google.gwt.core.ext.typeinfo.JClassType;
20  import com.google.gwt.core.ext.typeinfo.JType;
21  import com.google.gwt.core.ext.typeinfo.TypeOracle;
22  import com.google.gwt.core.ext.typeinfo.TypeOracleException;
23  import com.google.gwt.dom.client.Style.Unit;
24  import com.google.gwt.resources.client.ImageResource;
25  import com.google.gwt.safehtml.shared.SafeHtml;
26  import com.google.gwt.uibinder.attributeparsers.AttributeParser;
27  import com.google.gwt.uibinder.attributeparsers.AttributeParsers;
28  
29  import org.w3c.dom.Attr;
30  import org.w3c.dom.Element;
31  import org.w3c.dom.NamedNodeMap;
32  import org.w3c.dom.Node;
33  import org.w3c.dom.NodeList;
34  import org.w3c.dom.Text;
35  
36  import java.util.ArrayList;
37  import java.util.Collection;
38  import java.util.HashSet;
39  import java.util.List;
40  import java.util.Set;
41  
42  /**
43   * A wrapper for {@link Element} that limits the way parsers can interact with
44   * the XML document, and provides some convenience methods.
45   * <p>
46   * The main function of this wrapper is to ensure that parsers can only read
47   * elements and attributes by 'consuming' them, which removes the given value.
48   * This allows for a natural hierarchy among parsers -- more specific parsers
49   * will run first, and if they consume a value, less-specific parsers will not
50   * see it.
51   */
52  public class XMLElement {
53    /**
54     * Callback interface used by {@link #consumeInnerHtml(Interpreter)} and
55     * {@link #consumeChildElements(Interpreter)}.
56     */
57    public interface Interpreter<T> {
58      /**
59       * Given an XMLElement, return its filtered value.
60       *
61       * @throws UnableToCompleteException on error
62       */
63      T interpretElement(XMLElement elem) throws UnableToCompleteException;
64    }
65  
66    /**
67     * Extends {@link Interpreter} with a method to be called after all elements
68     * have been processed.
69     */
70    public interface PostProcessingInterpreter<T> extends Interpreter<T> {
71      String postProcess(String consumedText) throws UnableToCompleteException;
72    }
73  
74    private static class NoBrainInterpeter<T> implements Interpreter<T> {
75      private final T rtn;
76  
77      public NoBrainInterpeter(T rtn) {
78        this.rtn = rtn;
79      }
80  
81      public T interpretElement(XMLElement elem) {
82        return rtn;
83      }
84    }
85  
86    /**
87     * Represents the source location where the XMLElement was declared.
88     */
89    public static class Location {
90      private final String systemId;
91      private final int lineNumber;
92  
93      public Location(String systemId, int lineNumber) {
94        this.systemId = systemId;
95        this.lineNumber = lineNumber;
96      }
97  
98      public int getLineNumber() {
99        return lineNumber;
100     }
101 
102     public String getSystemId() {
103       return systemId;
104     }
105 
106     /**
107      * For debugging use only.
108      */
109     @Override
110     public String toString() {
111       return systemId + ":" + lineNumber;
112     }
113   }
114 
115   static final String LOCATION_KEY = "gwtLocation";
116 
117   private static final Set<String> NO_END_TAG = new HashSet<String>();
118 
119   private static final String[] EMPTY = new String[]{};
120 
121   private static void clearChildren(Element elem) {
122     // TODO(rjrjr) I'm nearly positive that anywhere this is called
123     // we should instead be calling assertNoBody
124     Node child;
125     while ((child = elem.getFirstChild()) != null) {
126       elem.removeChild(child);
127     }
128   }
129 
130   private final Element elem;
131   private final AttributeParsers attributeParsers;
132   // for legacy templates
133   @SuppressWarnings("deprecation")
134   private final com.google.gwt.uibinder.attributeparsers.BundleAttributeParsers bundleParsers;
135   private final TypeOracle oracle;
136 
137   private final MortalLogger logger;
138   private final String debugString;
139 
140   private final DesignTimeUtils designTime;
141 
142   private final XMLElementProvider provider;
143 
144   private JType booleanType;
145   private JType imageResourceType;
146   private JType doubleType;
147   private JType intType;
148   private JType stringType;
149   private JType safeHtmlType;
150   
151   {
152     // from com/google/gxp/compiler/schema/html.xml
153     NO_END_TAG.add("area");
154     NO_END_TAG.add("base");
155     NO_END_TAG.add("basefont");
156     NO_END_TAG.add("br");
157     NO_END_TAG.add("col");
158     NO_END_TAG.add("frame");
159     NO_END_TAG.add("hr");
160     NO_END_TAG.add("img");
161     NO_END_TAG.add("input");
162     NO_END_TAG.add("isindex");
163     NO_END_TAG.add("link");
164     NO_END_TAG.add("meta");
165     NO_END_TAG.add("param");
166     NO_END_TAG.add("wbr");
167   }
168 
169   // bundleParsers for legacy templates
170   @SuppressWarnings("deprecation")
171   XMLElement(
172       Element elem,
173       AttributeParsers attributeParsers,
174       com.google.gwt.uibinder.attributeparsers.BundleAttributeParsers bundleParsers,
175       TypeOracle oracle, MortalLogger logger, DesignTimeUtils designTime,
176       XMLElementProvider provider) {
177     this.elem = elem;
178     this.attributeParsers = attributeParsers;
179     this.bundleParsers = bundleParsers;
180     this.oracle = oracle;
181     this.logger = logger;
182     this.designTime = designTime;
183     this.provider = provider;
184 
185     this.debugString = getOpeningTag();
186   }
187 
188   /**
189    * Ensure that the receiver has no attributes left.
190    *
191    * @throws UnableToCompleteException if it does
192    */
193   public void assertNoAttributes() throws UnableToCompleteException {
194     int numAtts = getAttributeCount();
195     if (numAtts == 0) {
196       return;
197     }
198 
199     StringBuilder b = new StringBuilder();
200     for (int i = 0; i < numAtts; i++) {
201       if (i > 0) {
202         b.append(", ");
203       }
204       b.append('"').append(getAttribute(i).getName()).append('"');
205     }
206     logger.die(this, "Unexpected attributes: %s", b);
207   }
208 
209   /**
210    * Require that the receiver's body is empty of text and has no child nodes.
211    *
212    * @throws UnableToCompleteException if it isn't
213    */
214   public void assertNoBody() throws UnableToCompleteException {
215     consumeChildElements(new Interpreter<Boolean>() {
216       public Boolean interpretElement(XMLElement elem)
217           throws UnableToCompleteException {
218         logger.die(elem, "Found unexpected child element");
219         return false; // unreachable
220       }
221     });
222     assertNoText();
223   }
224 
225   /**
226    * Require that the receiver's body is empty of text.
227    *
228    * @throws UnableToCompleteException if it isn't
229    */
230   public void assertNoText() throws UnableToCompleteException {
231     NoBrainInterpeter<String> nullInterpreter = new NoBrainInterpeter<String>(
232         null);
233     String s = consumeInnerTextEscapedAsHtmlStringLiteral(nullInterpreter);
234     if (!"".equals(s)) {
235       logger.die(this, "Unexpected text in element: \"%s\"", s);
236     }
237   }
238 
239   /**
240    * Consumes the given attribute as a literal or field reference. The type
241    * parameter is required to determine how the value is parsed and validated.
242    *
243    * @param name the attribute's full name (including prefix)
244    * @param type the type this attribute is expected to provide
245    * @return the attribute's value as a Java expression, or null if it is not
246    *         set
247    * @throws UnableToCompleteException on parse failure
248    */
249   public String consumeAttribute(String name, JType type)
250       throws UnableToCompleteException {
251     return consumeAttributeWithDefault(name, null, type);
252   }
253 
254   /**
255    * Consumes the given attribute as a literal or field reference. The type
256    * parameter is required to determine how the value is parsed and validated.
257    *
258    * @param name the attribute's full name (including prefix)
259    * @param defaultValue the value to @return if the attribute was unset
260    * @param type the type this attribute is expected to provide
261    * @return the attribute's value as a Java expression, or the given default if
262    *         it was unset
263    * @throws UnableToCompleteException on parse failure
264    */
265   public String consumeAttributeWithDefault(String name, String defaultValue,
266       JType type) throws UnableToCompleteException {
267     return consumeAttributeWithDefault(name, defaultValue, new JType[]{type});
268   }
269 
270   /**
271    * Like {@link #consumeAttributeWithDefault(String, String, JType)}, but
272    * accommodates more complex type signatures.
273    */
274   public String consumeAttributeWithDefault(String name, String defaultValue,
275       JType[] types) throws UnableToCompleteException {
276     /*
277      * TODO(rjrjr) The only reason we need the attribute here is for getParser,
278      * and getParser only needs it for horrible old BundleAttributeParsers. When
279      * that dies, this gets much simpler.
280      */
281     XMLAttribute attribute = getAttribute(name);
282     if (attribute == null) {
283       if (defaultValue != null) {
284         designTime.putAttribute(this, name + ".default", defaultValue);
285       }
286       return defaultValue;
287     }
288     String rawValue = attribute.consumeRawValue();
289     AttributeParser parser = getParser(attribute, types);
290     if (parser == null) {
291       logger.die(this, "No such attribute %s", name);
292     }
293 
294     try {
295       String value = parser.parse(rawValue);
296       designTime.putAttribute(this, name, value);
297       return value;
298     } catch (UnableToCompleteException e) {
299       logger.die(this, "Cannot parse attribute %s", name);
300       throw e;
301     }
302   }
303 
304   /**
305    * Convenience method for parsing the named attribute as a boolean value or
306    * reference.
307    *
308    * @return an expression that will evaluate to a boolean value in the
309    *         generated code, or null if there is no such attribute
310    *
311    * @throws UnableToCompleteException on unparseable value
312    */
313   public String consumeBooleanAttribute(String name)
314       throws UnableToCompleteException {
315     return consumeAttribute(name, getBooleanType());
316   }
317 
318   /**
319    * Convenience method for parsing the named attribute as a boolean value or
320    * reference.
321    *
322    * @param defaultValue value to return if attribute was not set
323    * @return an expression that will evaluate to a boolean value in the
324    *         generated code, or defaultValue if there is no such attribute
325    *
326    * @throws UnableToCompleteException on unparseable value
327    */
328   public String consumeBooleanAttribute(String name, boolean defaultValue)
329       throws UnableToCompleteException {
330     return consumeAttributeWithDefault(name, Boolean.toString(defaultValue),
331         getBooleanType());
332   }
333 
334   /**
335    * Consumes the named attribute as a boolean expression. This will not accept
336    * {field.reference} expressions. Useful for values that must be resolved at
337    * compile time, such as generated annotation values.
338    *
339    * @return {@link Boolean#TRUE}, {@link Boolean#FALSE}, or null if no such
340    *         attribute
341    *
342    * @throws UnableToCompleteException on unparseable value
343    */
344   public Boolean consumeBooleanConstantAttribute(String name)
345       throws UnableToCompleteException {
346     String value = consumeRawAttribute(name);
347     if (value == null) {
348       return null;
349     }
350     if (value.equals("true") || value.equals("false")) {
351       return Boolean.valueOf(value);
352     }
353     logger.die(this, "%s must be \"true\" or \"false\"", name);
354     return null; // unreachable
355   }
356 
357   /**
358    * Consumes and returns all child elements.
359    *
360    * @throws UnableToCompleteException if extra text nodes are found
361    */
362   public Iterable<XMLElement> consumeChildElements()
363       throws UnableToCompleteException {
364     Iterable<XMLElement> rtn = consumeChildElementsNoEmptyCheck();
365     assertNoText();
366     return rtn;
367   }
368 
369   /**
370    * Consumes and returns all child elements selected by the interpreter. Note
371    * that text nodes are not elements, and so are not presented for
372    * interpretation, and are not consumed.
373    *
374    * @param interpreter Should return true for any child that should be consumed
375    *          and returned by the consumeChildElements call
376    * @throws UnableToCompleteException
377    */
378   public Collection<XMLElement> consumeChildElements(
379       Interpreter<Boolean> interpreter) throws UnableToCompleteException {
380     List<XMLElement> elements = new ArrayList<XMLElement>();
381     List<Node> doomed = new ArrayList<Node>();
382 
383     NodeList childNodes = elem.getChildNodes();
384     for (int i = 0; i < childNodes.getLength(); ++i) {
385       Node childNode = childNodes.item(i);
386       if (childNode.getNodeType() == Node.ELEMENT_NODE) {
387         XMLElement childElement = provider.get((Element) childNode);
388         if (interpreter.interpretElement(childElement)) {
389           elements.add(childElement);
390           doomed.add(childNode);
391         }
392       }
393     }
394 
395     for (Node n : doomed) {
396       elem.removeChild(n);
397     }
398     return elements;
399   }
400 
401   /**
402    * Convenience method for parsing the named attribute as an ImageResource
403    * value or reference.
404    *
405    * @return an expression that will evaluate to an ImageResource value in the
406    *         generated code, or null if there is no such attribute
407    * @throws UnableToCompleteException on unparseable value
408    */
409   public String consumeImageResourceAttribute(String name)
410       throws UnableToCompleteException {
411     return consumeAttribute(name, getImageResourceType());
412   }
413 
414   /**
415    * Consumes all child elements, and returns an HTML interpretation of them.
416    * Trailing and leading whitespace is trimmed.
417    * <p>
418    * Each element encountered will be passed to the given Interpreter for
419    * possible replacement. Escaping is performed to allow the returned text to
420    * serve as a Java string literal used as input to a setInnerHTML call.
421    * <p>
422    * This call requires an interpreter to make sense of any special children.
423    * The odds are you want to use
424    * {@link com.google.gwt.uibinder.elementparsers.HtmlInterpreter}
425    * for an HTML value, or
426    * {@link com.google.gwt.uibinder.elementparsers.TextInterpreter}
427    * for text.
428    *
429    * @param interpreter Called for each element, expected to return a string
430    *          replacement for it, or null if it should be left as is
431    */
432   public String consumeInnerHtml(Interpreter<String> interpreter)
433       throws UnableToCompleteException {
434     if (interpreter == null) {
435       throw new NullPointerException("interpreter must not be null");
436     }
437     StringBuffer buf = new StringBuffer();
438     GetInnerHtmlVisitor.getEscapedInnerHtml(elem, buf, interpreter, provider);
439 
440     clearChildren(elem);
441     return buf.toString().trim();
442   }
443 
444   /**
445    * Refines {@link #consumeInnerHtml(Interpreter)} to handle
446    * PostProcessingInterpreter.
447    */
448   public String consumeInnerHtml(PostProcessingInterpreter<String> interpreter)
449       throws UnableToCompleteException {
450     String html = consumeInnerHtml((Interpreter<String>) interpreter);
451     return interpreter.postProcess(html);
452   }
453 
454   /**
455    * Refines {@link #consumeInnerTextEscapedAsHtmlStringLiteral(Interpreter)} to
456    * handle PostProcessingInterpreter.
457    */
458   public String consumeInnerText(PostProcessingInterpreter<String> interpreter)
459       throws UnableToCompleteException {
460     String text = consumeInnerTextEscapedAsHtmlStringLiteral(interpreter);
461     return interpreter.postProcess(text);
462   }
463 
464   /**
465    * Consumes all child text nodes, and asserts that this element held only
466    * text. Trailing and leading whitespace is trimmed, and escaped for use as a
467    * string literal. Notice that HTML entities in the text are also escaped--is
468    * this a source of errors?
469    * <p>
470    * This call requires an interpreter to make sense of any special children.
471    * The odds are you want to use
472    * {@link com.google.gwt.uibinder.elementparsers.TextInterpreter}
473    *
474    * @throws UnableToCompleteException If any elements present are not consumed
475    *           by the interpreter
476    */
477   public String consumeInnerTextEscapedAsHtmlStringLiteral(
478       Interpreter<String> interpreter) throws UnableToCompleteException {
479     if (interpreter == null) {
480       throw new NullPointerException("interpreter must not be null");
481     }
482     StringBuffer buf = new StringBuffer();
483 
484     GetEscapedInnerTextVisitor.getEscapedInnerText(elem, buf, interpreter,
485         provider);
486 
487     // Make sure there are no children left but empty husks
488     for (XMLElement child : consumeChildElementsNoEmptyCheck()) {
489       if (child.hasChildNodes() || child.getAttributeCount() > 0) {
490         logger.die(this, "Illegal child %s in a text-only context. "
491                    + "Perhaps you are trying to use unescaped HTML "
492                    + "where text is required, as in a HasText widget?", child);
493       }
494     }
495 
496     clearChildren(elem);
497     return buf.toString().trim();
498   }
499 
500   /**
501    * Convenience method for parsing the named attribute as a CSS length value.
502    *
503    * @return a (double, Unit) pair literal, an expression that will evaluate to
504    *         such a pair in the generated code, or null if there is no such
505    *         attribute
506    *
507    * @throws UnableToCompleteException on unparseable value
508    */
509   public String consumeLengthAttribute(String name)
510       throws UnableToCompleteException {
511     return consumeAttributeWithDefault(name, null, new JType[]{
512         getDoubleType(), getUnitType()});
513   }
514 
515   /**
516    * Consumes all attributes, and returns a string representing the entire
517    * opening tag. E.g., "<div able='baker'>"
518    */
519   public String consumeOpeningTag() {
520     String rtn = getOpeningTag();
521 
522     for (int i = getAttributeCount() - 1; i >= 0; i--) {
523       getAttribute(i).consumeRawValue();
524     }
525     return rtn;
526   }
527 
528   /**
529    * Consumes the named attribute and parses it to an unparsed, unescaped array
530    * of Strings. The strings in the attribute may be comma or space separated
531    * (or a mix of both).
532    *
533    * @return array of String, empty if the attribute was not set.
534    */
535   public String[] consumeRawArrayAttribute(String name) {
536     String raw = consumeRawAttribute(name, null);
537     if (raw == null) {
538       return EMPTY;
539     }
540 
541     return raw.split("[,\\s]+");
542   }
543 
544   /**
545    * Consumes the given attribute and returns its trimmed value, or null if it
546    * was unset. The returned string is not escaped.
547    *
548    * @param name the attribute's full name (including prefix)
549    * @return the attribute's value, or ""
550    */
551   public String consumeRawAttribute(String name) {
552     if (!elem.hasAttribute(name)) {
553       return null;
554     }
555     String value = elem.getAttribute(name);
556     elem.removeAttribute(name);
557     return value.trim();
558   }
559 
560   /**
561    * Consumes the given attribute and returns its trimmed value, or the given
562    * default value if it was unset. The returned string is not escaped.
563    *
564    * @param name the attribute's full name (including prefix)
565    * @param defaultValue the value to return if the attribute was unset
566    * @return the attribute's value, or defaultValue
567    */
568   public String consumeRawAttribute(String name, String defaultValue) {
569     String value = consumeRawAttribute(name);
570     if (value == null) {
571       return defaultValue;
572     }
573     return value;
574   }
575 
576   /**
577    * Consumes the given required attribute as a literal or field reference. The
578    * types parameters are required to determine how the value is parsed and
579    * validated.
580    *
581    * @param name the attribute's full name (including prefix)
582    * @param types the type(s) this attribute is expected to provide
583    * @return the attribute's value as a Java expression
584    * @throws UnableToCompleteException on parse failure, or if the attribute is
585    *           empty or unspecified
586    */
587   public String consumeRequiredAttribute(String name, JType... types)
588       throws UnableToCompleteException {
589     /*
590      * TODO(rjrjr) We have to get the attribute to get the parser, and we must
591      * get the attribute before we consume the value. This nasty subtlety is all
592      * down to BundleParsers, which we'll hopefully kill off soon.
593      */
594     XMLAttribute attribute = getAttribute(name);
595     if (attribute == null) {
596       failRequired(name);
597     }
598     AttributeParser parser = getParser(attribute, types);
599     String rawValue = consumeRequiredRawAttribute(name);
600 
601     try {
602       String value = parser.parse(rawValue);
603       designTime.putAttribute(this, name, value);
604       return value;
605     } catch (UnableToCompleteException e) {
606       logger.die(this, "Cannot parse attribute \"%s\"", name);
607       throw e;
608     }
609   }
610 
611   /**
612    * Convenience method for parsing the named required attribute as a double
613    * value or reference.
614    *
615    * @return a double literal, an expression that will evaluate to a double
616    *         value in the generated code
617    *
618    * @throws UnableToCompleteException on unparseable value, or if the attribute
619    *           is empty or unspecified
620    */
621   public String consumeRequiredDoubleAttribute(String name)
622       throws UnableToCompleteException {
623     return consumeRequiredAttribute(name, getDoubleType());
624   }
625 
626   /**
627    * Convenience method for parsing the named required attribute as a integer
628    * value or reference.
629    *
630    * @return a integer literal, an expression that will evaluate to a integer
631    *         value in the generated code
632    *
633    * @throws UnableToCompleteException on unparseable value, or if the attribute
634    *           is empty or unspecified
635    */
636   public String consumeRequiredIntAttribute(String name)
637       throws UnableToCompleteException {
638     return consumeRequiredAttribute(name, getIntType());
639   }
640 
641   /**
642    * Consumes the named attribute, or dies if it is missing.
643    */
644   public String consumeRequiredRawAttribute(String name)
645       throws UnableToCompleteException {
646     String value = consumeRawAttribute(name);
647     if (value == null) {
648       failRequired(name);
649     }
650     return value;
651   }
652   
653   /**
654   * Convenience method for parsing the named attribute as a 
655   * {@link com.google.gwt.safehtml.shared.SafeHtml SafeHtml} value or reference.
656   *
657   * @return an expression that will evaluate to a
658   * {@link com.google.gwt.safehtml.shared.SafeHtml SafeHtml} value in 
659   * the generated code, or null if there is no such attribute
660   * @throws UnableToCompleteException on unparseable value
661   */
662   public String consumeSafeHtmlAttribute(String name)
663       throws UnableToCompleteException {
664     return consumeAttribute(name, getSafeHtmlType());
665   }
666   /**
667    * Consumes a single child element, ignoring any text nodes and throwing an
668    * exception if no child is found, or more than one child element is found.
669    *
670    * @throws UnableToCompleteException on no children, or too many
671    */
672   public XMLElement consumeSingleChildElement()
673       throws UnableToCompleteException {
674     XMLElement ret = null;
675     for (XMLElement child : consumeChildElements()) {
676       if (ret != null) {
677         logger.die(this,
678             "Element may only contain a single child element, but "
679                 + "found %s and %s.", ret, child);
680       }
681 
682       ret = child;
683     }
684 
685     if (ret == null) {
686       logger.die(this, "Element must have a single child element");
687     }
688 
689     return ret;
690   }
691 
692   /**
693    * Consumes the named attribute and parses it to an array of String
694    * expressions. The strings in the attribute may be comma or space separated
695    * (or a mix of both).
696    *
697    * @return array of String expressions, empty if the attribute was not set.
698    * @throws UnableToCompleteException on unparseable value
699    */
700   public String[] consumeStringArrayAttribute(String name)
701       throws UnableToCompleteException {
702     AttributeParser parser = attributeParsers.get(getStringType());
703 
704     String[] strings = consumeRawArrayAttribute(name);
705     for (int i = 0; i < strings.length; i++) {
706       try {
707         strings[i] = parser.parse(strings[i]);
708       } catch (UnableToCompleteException e) {
709         logger.die(this, "Cannot parse attribute " + name);
710         throw e;
711       }
712     }
713     designTime.putAttribute(this, name, strings);
714     return strings;
715   }
716 
717   /**
718    * Convenience method for parsing the named attribute as a String value or
719    * reference.
720    *
721    * @return an expression that will evaluate to a String value in the generated
722    *         code, or null if there is no such attribute
723    * @throws UnableToCompleteException on unparseable value
724    */
725   public String consumeStringAttribute(String name)
726       throws UnableToCompleteException {
727     return consumeAttribute(name, getStringType());
728   }
729 
730   /**
731    * Convenience method for parsing the named attribute as a String value or
732    * reference.
733    *
734    * @return an expression that will evaluate to a String value in the generated
735    *         code, or the given defaultValue if there is no such attribute
736    * @throws UnableToCompleteException on unparseable value
737    */
738   public String consumeStringAttribute(String name, String defaultValue)
739       throws UnableToCompleteException {
740     return consumeAttributeWithDefault(name, defaultValue, getStringType());
741   }
742 
743   /**
744    * Returns the unprocessed, unescaped, raw inner text of the receiver. Dies if
745    * the receiver has non-text children.
746    * <p>
747    * You probably want to use
748    * {@link #consumeInnerTextEscapedAsHtmlStringLiteral} instead.
749    *
750    * @return the text
751    * @throws UnableToCompleteException if it held anything other than text nodes
752    */
753   public String consumeUnescapedInnerText() throws UnableToCompleteException {
754     final NodeList children = elem.getChildNodes();
755     if (children.getLength() < 1) {
756       return "";
757     }
758     if (children.getLength() > 1
759         || Node.TEXT_NODE != children.item(0).getNodeType()) {
760       logger.die(this, "Element must contain only text");
761     }
762     Text t = (Text) children.item(0);
763     return t.getTextContent();
764   }
765 
766   /**
767    * Get the attribute at the given index. If you are consuming attributes,
768    * remember to traverse them in reverse.
769    */
770   public XMLAttribute getAttribute(int i) {
771     return new XMLAttribute(XMLElement.this,
772         (Attr) elem.getAttributes().item(i));
773   }
774 
775   /**
776    * Get the attribute with the given name.
777    *
778    * @return the attribute, or null if there is none of that name
779    */
780   public XMLAttribute getAttribute(String name) {
781     Attr attr = elem.getAttributeNode(name);
782     if (attr == null) {
783       return null;
784     }
785     return new XMLAttribute(this, attr);
786   }
787 
788   /**
789    * Returns the number of attributes this element has.
790    */
791   public int getAttributeCount() {
792     return elem.getAttributes().getLength();
793   }
794 
795   public String getClosingTag() {
796     if (NO_END_TAG.contains(elem.getTagName())) {
797       return "";
798     }
799     return String.format("</%s>", elem.getTagName());
800   }
801 
802   /**
803    * Returns the design time path of this element, in form of indexes from root,
804    * such as "0/0/1/0".
805    */
806   public String getDesignTimePath() {
807     return designTime.getPath(elem);
808   }
809 
810   /**
811    * Gets this element's local name (sans namespace prefix).
812    */
813   public String getLocalName() {
814     return elem.getLocalName();
815   }
816 
817   public Location getLocation() {
818     return (Location) elem.getUserData(LOCATION_KEY);
819   }
820 
821   /**
822    * Gets this element's namespace URI.
823    */
824   public String getNamespaceUri() {
825     return elem.getNamespaceURI();
826   }
827 
828   public String getNamespaceUriForAttribute(String fieldName) {
829     Attr attr = elem.getAttributeNode(fieldName);
830     return attr.getNamespaceURI();
831   }
832 
833   /**
834    * Returns the parent element, or null if parent is null or a node type other
835    * than Element.
836    */
837   public XMLElement getParent() {
838     Node parent = elem.getParentNode();
839     if (parent == null || Node.ELEMENT_NODE != parent.getNodeType()) {
840       return null;
841     }
842     return provider.get((Element) parent);
843   }
844 
845   public String getPrefix() {
846     return elem.getPrefix();
847   }
848 
849   /**
850    * Determines whether the element has a given attribute.
851    */
852   public boolean hasAttribute(String name) {
853     return elem.hasAttribute(name);
854   }
855 
856   public boolean hasChildNodes() {
857     return elem.hasChildNodes();
858   }
859 
860   public String lookupPrefix(String prefix) {
861     return elem.lookupPrefix(prefix);
862   }
863 
864   public void setAttribute(String name, String value) {
865     elem.setAttribute(name, value);
866   }
867 
868   @Override
869   public String toString() {
870     return debugString;
871   }
872 
873   private Iterable<XMLElement> consumeChildElementsNoEmptyCheck() {
874     try {
875       Iterable<XMLElement> rtn = consumeChildElements(new NoBrainInterpeter<Boolean>(
876           true));
877       return rtn;
878     } catch (UnableToCompleteException e) {
879       throw new RuntimeException("Impossible exception", e);
880     }
881   }
882 
883   private void failRequired(String name) throws UnableToCompleteException {
884     logger.die(this, "Missing required attribute \"%s\"", name);
885   }
886 
887   private JType getBooleanType() {
888     if (booleanType == null) {
889       try {
890         booleanType = oracle.parse("boolean");
891       } catch (TypeOracleException e) {
892         throw new RuntimeException(e);
893       }
894     }
895     return booleanType;
896   }
897 
898   private JType getDoubleType() {
899     if (doubleType == null) {
900       try {
901         doubleType = oracle.parse("double");
902       } catch (TypeOracleException e) {
903         throw new RuntimeException(e);
904       }
905     }
906     return doubleType;
907   }
908 
909   private JType getImageResourceType() {
910     if (imageResourceType == null) {
911       imageResourceType = oracle.findType(ImageResource.class.getCanonicalName());
912     }
913     return imageResourceType;
914   }
915 
916   private JType getIntType() {
917     if (intType == null) {
918       try {
919         intType = oracle.parse("int");
920       } catch (TypeOracleException e) {
921         throw new RuntimeException(e);
922       }
923     }
924     return intType;
925   }
926 
927   private String getOpeningTag() {
928     StringBuilder b = new StringBuilder().append("<").append(elem.getTagName());
929 
930     NamedNodeMap attrs = elem.getAttributes();
931     for (int i = 0; i < attrs.getLength(); i++) {
932       Attr attr = (Attr) attrs.item(i);
933       b.append(String.format(" %s='%s'", attr.getName(),
934           UiBinderWriter.escapeAttributeText(attr.getValue())));
935     }
936     b.append(">");
937     return b.toString();
938   }
939 
940   @SuppressWarnings("deprecation")
941   private AttributeParser getParser(XMLAttribute xmlAttribute, JType... types)
942       throws UnableToCompleteException {
943     AttributeParser rtn = bundleParsers.get(xmlAttribute);
944     if (rtn == null) {
945       rtn = attributeParsers.get(types);
946     }
947 
948     return rtn;
949   }
950 
951   private JType getSafeHtmlType() {
952     if (safeHtmlType == null) {
953       safeHtmlType = oracle.findType(SafeHtml.class.getName());
954     }
955     return safeHtmlType;
956   }
957   
958   private JType getStringType() {
959     if (stringType == null) {
960       stringType = oracle.findType(String.class.getCanonicalName());
961     }
962     return stringType;
963   }
964 
965   private JClassType getUnitType() {
966     return oracle.findType(Unit.class.getCanonicalName()).isEnum();
967   }
968   // laaglu
969   public Element getElement() {
970     return elem;
971   }
972   // laaglu
973 }