1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
44
45
46
47
48
49
50
51
52 public class XMLElement {
53
54
55
56
57 public interface Interpreter<T> {
58
59
60
61
62
63 T interpretElement(XMLElement elem) throws UnableToCompleteException;
64 }
65
66
67
68
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
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
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
123
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
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
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
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
190
191
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
211
212
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;
220 }
221 });
222 assertNoText();
223 }
224
225
226
227
228
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
241
242
243
244
245
246
247
248
249 public String consumeAttribute(String name, JType type)
250 throws UnableToCompleteException {
251 return consumeAttributeWithDefault(name, null, type);
252 }
253
254
255
256
257
258
259
260
261
262
263
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
272
273
274 public String consumeAttributeWithDefault(String name, String defaultValue,
275 JType[] types) throws UnableToCompleteException {
276
277
278
279
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
306
307
308
309
310
311
312
313 public String consumeBooleanAttribute(String name)
314 throws UnableToCompleteException {
315 return consumeAttribute(name, getBooleanType());
316 }
317
318
319
320
321
322
323
324
325
326
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
336
337
338
339
340
341
342
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;
355 }
356
357
358
359
360
361
362 public Iterable<XMLElement> consumeChildElements()
363 throws UnableToCompleteException {
364 Iterable<XMLElement> rtn = consumeChildElementsNoEmptyCheck();
365 assertNoText();
366 return rtn;
367 }
368
369
370
371
372
373
374
375
376
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
403
404
405
406
407
408
409 public String consumeImageResourceAttribute(String name)
410 throws UnableToCompleteException {
411 return consumeAttribute(name, getImageResourceType());
412 }
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
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
446
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
456
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
466
467
468
469
470
471
472
473
474
475
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
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
502
503
504
505
506
507
508
509 public String consumeLengthAttribute(String name)
510 throws UnableToCompleteException {
511 return consumeAttributeWithDefault(name, null, new JType[]{
512 getDoubleType(), getUnitType()});
513 }
514
515
516
517
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
530
531
532
533
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
546
547
548
549
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
562
563
564
565
566
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
578
579
580
581
582
583
584
585
586
587 public String consumeRequiredAttribute(String name, JType... types)
588 throws UnableToCompleteException {
589
590
591
592
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
613
614
615
616
617
618
619
620
621 public String consumeRequiredDoubleAttribute(String name)
622 throws UnableToCompleteException {
623 return consumeRequiredAttribute(name, getDoubleType());
624 }
625
626
627
628
629
630
631
632
633
634
635
636 public String consumeRequiredIntAttribute(String name)
637 throws UnableToCompleteException {
638 return consumeRequiredAttribute(name, getIntType());
639 }
640
641
642
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
655
656
657
658
659
660
661
662 public String consumeSafeHtmlAttribute(String name)
663 throws UnableToCompleteException {
664 return consumeAttribute(name, getSafeHtmlType());
665 }
666
667
668
669
670
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
694
695
696
697
698
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
719
720
721
722
723
724
725 public String consumeStringAttribute(String name)
726 throws UnableToCompleteException {
727 return consumeAttribute(name, getStringType());
728 }
729
730
731
732
733
734
735
736
737
738 public String consumeStringAttribute(String name, String defaultValue)
739 throws UnableToCompleteException {
740 return consumeAttributeWithDefault(name, defaultValue, getStringType());
741 }
742
743
744
745
746
747
748
749
750
751
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
768
769
770 public XMLAttribute getAttribute(int i) {
771 return new XMLAttribute(XMLElement.this,
772 (Attr) elem.getAttributes().item(i));
773 }
774
775
776
777
778
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
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
804
805
806 public String getDesignTimePath() {
807 return designTime.getPath(elem);
808 }
809
810
811
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
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
835
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
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
969 public Element getElement() {
970 return elem;
971 }
972
973 }