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 import com.google.gwt.uibinder.elementparsers.SimpleInterpeter;
29
30 import org.w3c.dom.Attr;
31 import org.w3c.dom.Element;
32 import org.w3c.dom.NamedNodeMap;
33 import org.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35 import org.w3c.dom.Text;
36
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Set;
42
43
44
45
46
47
48
49
50
51
52
53 public class XMLElement {
54
55
56
57
58 public interface Interpreter<T> {
59
60
61
62
63
64 T interpretElement(XMLElement elem) throws UnableToCompleteException;
65 }
66
67
68
69
70
71 public interface PostProcessingInterpreter<T> extends Interpreter<T> {
72 String postProcess(String consumedText) throws UnableToCompleteException;
73 }
74
75
76
77
78 public static class Location {
79 private final String systemId;
80 private final int lineNumber;
81
82 public Location(String systemId, int lineNumber) {
83 this.systemId = systemId;
84 this.lineNumber = lineNumber;
85 }
86
87 public int getLineNumber() {
88 return lineNumber;
89 }
90
91 public String getSystemId() {
92 return systemId;
93 }
94
95
96
97
98 @Override
99 public String toString() {
100 return systemId + ":" + lineNumber;
101 }
102 }
103
104 static final String LOCATION_KEY = "gwtLocation";
105
106 private static final Set<String> NO_END_TAG = new HashSet<String>();
107
108 private static final String[] EMPTY = new String[]{};
109
110 private static void clearChildren(Element elem) {
111
112
113 Node child;
114 while ((child = elem.getFirstChild()) != null) {
115 elem.removeChild(child);
116 }
117 }
118
119 private final Element elem;
120 private final AttributeParsers attributeParsers;
121 private final TypeOracle oracle;
122
123 private final MortalLogger logger;
124 private final String debugString;
125
126 private final DesignTimeUtils designTime;
127
128 private final XMLElementProvider provider;
129
130 private JType booleanType;
131 private JType imageResourceType;
132 private JType doubleType;
133 private JType intType;
134 private JType safeHtmlType;
135 private JType stringType;
136
137 {
138
139 NO_END_TAG.add("area");
140 NO_END_TAG.add("base");
141 NO_END_TAG.add("basefont");
142 NO_END_TAG.add("br");
143 NO_END_TAG.add("col");
144 NO_END_TAG.add("frame");
145 NO_END_TAG.add("hr");
146 NO_END_TAG.add("img");
147 NO_END_TAG.add("input");
148 NO_END_TAG.add("isindex");
149 NO_END_TAG.add("link");
150 NO_END_TAG.add("meta");
151 NO_END_TAG.add("param");
152 NO_END_TAG.add("wbr");
153 }
154
155 XMLElement(Element elem, AttributeParsers attributeParsers, TypeOracle oracle,
156 MortalLogger logger, DesignTimeUtils designTime, XMLElementProvider provider) {
157 this.elem = elem;
158 this.attributeParsers = attributeParsers;
159 this.oracle = oracle;
160 this.logger = logger;
161 this.designTime = designTime;
162 this.provider = provider;
163
164 this.debugString = getOpeningTag();
165 }
166
167
168
169
170
171
172 public void assertNoAttributes() throws UnableToCompleteException {
173 int numAtts = getAttributeCount();
174 if (numAtts == 0) {
175 return;
176 }
177
178 StringBuilder b = new StringBuilder();
179 for (int i = 0; i < numAtts; i++) {
180 if (i > 0) {
181 b.append(", ");
182 }
183 b.append('"').append(getAttribute(i).getName()).append('"');
184 }
185 logger.die(this, "Unexpected attributes: %s", b);
186 }
187
188
189
190
191
192
193 public void assertNoBody() throws UnableToCompleteException {
194 consumeChildElements(new Interpreter<Boolean>() {
195 public Boolean interpretElement(XMLElement elem) throws UnableToCompleteException {
196 logger.die(elem, "Found unexpected child element");
197 return false;
198 }
199 });
200 assertNoText();
201 }
202
203
204
205
206
207
208 public void assertNoText() throws UnableToCompleteException {
209 SimpleInterpeter<String> nullInterpreter = new SimpleInterpeter<String>(null);
210 String s = consumeInnerTextEscapedAsHtmlStringLiteral(nullInterpreter);
211 if (!"".equals(s)) {
212 logger.die(this, "Unexpected text in element: \"%s\"", s);
213 }
214 }
215
216
217
218
219
220
221
222
223
224
225
226 public String consumeAttribute(String name, JType type) throws UnableToCompleteException {
227 return consumeAttributeWithDefault(name, null, type);
228 }
229
230
231
232
233
234
235
236
237
238
239
240
241 public String consumeAttributeWithDefault(String name, String defaultValue, JType type)
242 throws UnableToCompleteException {
243 return consumeAttributeWithDefault(name, defaultValue, new JType[]{type});
244 }
245
246
247
248
249
250 public String consumeAttributeWithDefault(String name, String defaultValue, JType... types)
251 throws UnableToCompleteException {
252
253 if (!hasAttribute(name)) {
254 if (defaultValue != null) {
255 designTime.putAttribute(this, name + ".default", defaultValue);
256 }
257 return defaultValue;
258 }
259
260 AttributeParser parser = attributeParsers.getParser(types);
261 return consumeAttributeWithParser(name, parser);
262 }
263
264
265
266
267
268
269
270
271
272
273 public String consumeBooleanAttribute(String name) throws UnableToCompleteException {
274 return consumeAttribute(name, getBooleanType());
275 }
276
277
278
279
280
281
282
283
284
285
286
287 public String consumeBooleanAttribute(String name, boolean defaultValue)
288 throws UnableToCompleteException {
289 return consumeAttributeWithDefault(name, Boolean.toString(defaultValue), getBooleanType());
290 }
291
292
293
294
295
296
297
298
299
300
301
302 public Boolean consumeBooleanConstantAttribute(String name) throws UnableToCompleteException {
303 String value = consumeRawAttribute(name);
304 if (value == null) {
305 return null;
306 }
307 if (value.equals("true") || value.equals("false")) {
308 return Boolean.valueOf(value);
309 }
310 logger.die(this, "%s must be \"true\" or \"false\"", name);
311 return null;
312 }
313
314
315
316
317
318
319 public Iterable<XMLElement> consumeChildElements() throws UnableToCompleteException {
320 Iterable<XMLElement> rtn = consumeChildElementsNoEmptyCheck();
321 assertNoText();
322 return rtn;
323 }
324
325
326
327
328
329
330
331
332
333
334 public Collection<XMLElement> consumeChildElements(Interpreter<Boolean> interpreter)
335 throws UnableToCompleteException {
336 List<XMLElement> elements = new ArrayList<XMLElement>();
337 List<Node> doomed = new ArrayList<Node>();
338
339 NodeList childNodes = elem.getChildNodes();
340 for (int i = 0; i < childNodes.getLength(); ++i) {
341 Node childNode = childNodes.item(i);
342 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
343 XMLElement childElement = provider.get((Element) childNode);
344 if (interpreter.interpretElement(childElement)) {
345 elements.add(childElement);
346 doomed.add(childNode);
347 }
348 }
349 }
350
351 for (Node n : doomed) {
352 elem.removeChild(n);
353 }
354 return elements;
355 }
356
357
358
359
360
361
362
363
364
365 public String consumeImageResourceAttribute(String name) throws UnableToCompleteException {
366 return consumeAttribute(name, getImageResourceType());
367 }
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386 public String consumeInnerHtml(Interpreter<String> interpreter) throws UnableToCompleteException {
387 if (interpreter == null) {
388 throw new NullPointerException("interpreter must not be null");
389 }
390 StringBuffer buf = new StringBuffer();
391 GetInnerHtmlVisitor.getEscapedInnerHtml(elem, buf, interpreter, provider);
392
393 clearChildren(elem);
394 return buf.toString().trim();
395 }
396
397
398
399
400
401 public String consumeInnerHtml(PostProcessingInterpreter<String> interpreter)
402 throws UnableToCompleteException {
403 String html = consumeInnerHtml((Interpreter<String>) interpreter);
404 return interpreter.postProcess(html);
405 }
406
407
408
409
410
411 public String consumeInnerText(PostProcessingInterpreter<String> interpreter)
412 throws UnableToCompleteException {
413 String text = consumeInnerTextEscapedAsHtmlStringLiteral(interpreter);
414 return interpreter.postProcess(text);
415 }
416
417
418
419
420
421
422
423
424
425
426
427
428
429 public String consumeInnerTextEscapedAsHtmlStringLiteral(Interpreter<String> interpreter)
430 throws UnableToCompleteException {
431 return consumeInnerTextEscapedAsHtmlStringLiteral(interpreter, true);
432 }
433
434
435
436
437
438
439
440
441
442
443
444
445
446 public String consumeInnerTextEscapedAsStringLiteral(Interpreter<String> interpreter)
447 throws UnableToCompleteException {
448 return consumeInnerTextEscapedAsHtmlStringLiteral(interpreter, false);
449 }
450
451
452
453
454
455
456
457
458
459
460 public String consumeLengthAttribute(String name) throws UnableToCompleteException {
461 return consumeAttributeWithDefault(name, null, new JType[]{getDoubleType(), getUnitType()});
462 }
463
464
465
466
467
468 public String consumeOpeningTag() {
469 String rtn = getOpeningTag();
470
471 for (int i = getAttributeCount() - 1; i >= 0; i--) {
472 getAttribute(i).consumeRawValue();
473 }
474 return rtn;
475 }
476
477
478
479
480
481
482
483
484 public String[] consumeRawArrayAttribute(String name) {
485 String raw = consumeRawAttribute(name, null);
486 if (raw == null) {
487 return EMPTY;
488 }
489
490 return raw.split("[,\\s]+");
491 }
492
493
494
495
496
497
498
499
500 public String consumeRawAttribute(String name) {
501 if (!elem.hasAttribute(name)) {
502 return null;
503 }
504 String value = elem.getAttribute(name);
505 elem.removeAttribute(name);
506 return value.trim();
507 }
508
509
510
511
512
513
514
515
516
517 public String consumeRawAttribute(String name, String defaultValue) {
518 String value = consumeRawAttribute(name);
519 if (value == null) {
520 return defaultValue;
521 }
522 return value;
523 }
524
525
526
527
528
529
530
531
532
533
534
535
536 public String consumeRequiredAttribute(String name, JType... types)
537 throws UnableToCompleteException {
538 if (!hasAttribute(name)) {
539 failRequired(name);
540 }
541
542 AttributeParser parser = attributeParsers.getParser(types);
543
544 String value = parser.parse(this, consumeRequiredRawAttribute(name));
545 designTime.putAttribute(this, name, value);
546 return value;
547 }
548
549
550
551
552
553
554
555
556
557
558
559 public String consumeRequiredDoubleAttribute(String name) throws UnableToCompleteException {
560 return consumeRequiredAttribute(name, getDoubleType());
561 }
562
563
564
565
566
567
568
569
570
571
572
573 public String consumeRequiredIntAttribute(String name) throws UnableToCompleteException {
574 return consumeRequiredAttribute(name, getIntType());
575 }
576
577
578
579
580 public String consumeRequiredRawAttribute(String name) throws UnableToCompleteException {
581 String value = consumeRawAttribute(name);
582 if (value == null) {
583 failRequired(name);
584 }
585 return value;
586 }
587
588
589
590
591
592
593
594
595
596
597
598 public String consumeSafeHtmlAttribute(String name) throws UnableToCompleteException {
599 return consumeAttribute(name, getSafeHtmlType());
600 }
601
602
603
604
605
606
607
608
609
610 public String consumeSafeUriOrStringAttribute(String name) throws UnableToCompleteException {
611 return consumeAttributeWithParser(name, attributeParsers.getSafeUriInHtmlParser());
612 }
613
614
615
616
617
618
619
620 public XMLElement consumeSingleChildElement() throws UnableToCompleteException {
621 XMLElement ret = null;
622 for (XMLElement child : consumeChildElements()) {
623 if (ret != null) {
624 logger.die(this, "Element may only contain a single child element, but "
625 + "found %s and %s.", ret, child);
626 }
627
628 ret = child;
629 }
630
631 if (ret == null) {
632 logger.die(this, "Element must have a single child element");
633 }
634
635 return ret;
636 }
637
638
639
640
641
642
643
644
645
646 public String[] consumeStringArrayAttribute(String name) throws UnableToCompleteException {
647 AttributeParser parser = attributeParsers.getParser(getStringType());
648
649 String[] strings = consumeRawArrayAttribute(name);
650 for (int i = 0; i < strings.length; i++) {
651 strings[i] = parser.parse(this, strings[i]);
652 }
653 designTime.putAttribute(this, name, strings);
654 return strings;
655 }
656
657
658
659
660
661
662
663
664
665 public String consumeStringAttribute(String name) throws UnableToCompleteException {
666 return consumeAttribute(name, getStringType());
667 }
668
669
670
671
672
673
674
675
676
677 public String consumeStringAttribute(String name, String defaultValue)
678 throws UnableToCompleteException {
679 return consumeAttributeWithDefault(name, defaultValue, getStringType());
680 }
681
682
683
684
685
686
687
688
689
690
691
692 public String consumeUnescapedInnerText() throws UnableToCompleteException {
693 final NodeList children = elem.getChildNodes();
694 if (children.getLength() < 1) {
695 return "";
696 }
697 if (children.getLength() > 1 || Node.TEXT_NODE != children.item(0).getNodeType()) {
698 logger.die(this, "Element must contain only text");
699 }
700 Text t = (Text) children.item(0);
701 return t.getTextContent();
702 }
703
704
705
706
707
708 public XMLAttribute getAttribute(int i) {
709 return new XMLAttribute(XMLElement.this, (Attr) elem.getAttributes().item(i));
710 }
711
712
713
714
715
716
717 public XMLAttribute getAttribute(String name) {
718 Attr attr = elem.getAttributeNode(name);
719 if (attr == null) {
720 return null;
721 }
722 return new XMLAttribute(this, attr);
723 }
724
725
726
727
728 public int getAttributeCount() {
729 return elem.getAttributes().getLength();
730 }
731
732 public String getClosingTag() {
733 if (NO_END_TAG.contains(elem.getTagName())) {
734 return "";
735 }
736 return String.format("</%s>", elem.getTagName());
737 }
738
739
740
741
742
743 public String getDesignTimePath() {
744 return designTime.getPath(elem);
745 }
746
747
748
749
750 public String getLocalName() {
751 return elem.getLocalName();
752 }
753
754 public Location getLocation() {
755 return (Location) elem.getUserData(LOCATION_KEY);
756 }
757
758
759
760
761 public String getNamespaceUri() {
762 return elem.getNamespaceURI();
763 }
764
765
766
767
768
769 public XMLElement getParent() {
770 Node parent = elem.getParentNode();
771 if (parent == null || Node.ELEMENT_NODE != parent.getNodeType()) {
772 return null;
773 }
774 return provider.get((Element) parent);
775 }
776
777 public String getPrefix() {
778 return elem.getPrefix();
779 }
780
781
782
783
784 public boolean hasAttribute(String name) {
785 return elem.hasAttribute(name);
786 }
787
788 public boolean hasChildNodes() {
789 return elem.hasChildNodes();
790 }
791
792 public String lookupPrefix(String prefix) {
793 return elem.lookupPrefix(prefix);
794 }
795
796 public void setAttribute(String name, String value) {
797 elem.setAttribute(name, value);
798 }
799
800 @Override
801 public String toString() {
802 return debugString;
803 }
804
805 private String consumeAttributeWithParser(String name, AttributeParser parser)
806 throws UnableToCompleteException {
807 String value = parser.parse(this, consumeRawAttribute(name));
808 designTime.putAttribute(this, name, value);
809 return value;
810 }
811
812 private Iterable<XMLElement> consumeChildElementsNoEmptyCheck() {
813 try {
814 Iterable<XMLElement> rtn = consumeChildElements(new SimpleInterpeter<Boolean>(true));
815 return rtn;
816 } catch (UnableToCompleteException e) {
817 throw new RuntimeException("Impossible exception", e);
818 }
819 }
820
821
822
823
824
825
826
827
828
829
830
831
832
833 private String consumeInnerTextEscapedAsHtmlStringLiteral(Interpreter<String> interpreter,
834 boolean escapeHtmlEntities)
835 throws UnableToCompleteException {
836 if (interpreter == null) {
837 throw new NullPointerException("interpreter must not be null");
838 }
839 StringBuffer buf = new StringBuffer();
840
841 if (escapeHtmlEntities) {
842 GetInnerTextVisitor.getHtmlEscapedInnerText(elem, buf, interpreter, provider);
843 } else {
844 GetInnerTextVisitor.getEscapedInnerText(elem, buf, interpreter, provider);
845 }
846
847
848 for (XMLElement child : consumeChildElementsNoEmptyCheck()) {
849 if (child.hasChildNodes() || child.getAttributeCount() > 0) {
850 logger.die(this, "Illegal child %s in a text-only context. "
851 + "Perhaps you are trying to use unescaped HTML "
852 + "where text is required, as in a HasText widget?", child);
853 }
854 }
855
856 clearChildren(elem);
857 return buf.toString().trim();
858 }
859
860 private void failRequired(String name) throws UnableToCompleteException {
861 logger.die(this, "Missing required attribute \"%s\"", name);
862 }
863
864 private JType getBooleanType() {
865 if (booleanType == null) {
866 try {
867 booleanType = oracle.parse("boolean");
868 } catch (TypeOracleException e) {
869 throw new RuntimeException(e);
870 }
871 }
872 return booleanType;
873 }
874
875 private JType getDoubleType() {
876 if (doubleType == null) {
877 try {
878 doubleType = oracle.parse("double");
879 } catch (TypeOracleException e) {
880 throw new RuntimeException(e);
881 }
882 }
883 return doubleType;
884 }
885
886 private JType getImageResourceType() {
887 if (imageResourceType == null) {
888 imageResourceType = oracle.findType(ImageResource.class.getCanonicalName());
889 }
890 return imageResourceType;
891 }
892
893 private JType getIntType() {
894 if (intType == null) {
895 try {
896 intType = oracle.parse("int");
897 } catch (TypeOracleException e) {
898 throw new RuntimeException(e);
899 }
900 }
901 return intType;
902 }
903
904 private String getOpeningTag() {
905 StringBuilder b = new StringBuilder().append("<").append(elem.getTagName());
906
907 NamedNodeMap attrs = elem.getAttributes();
908 for (int i = 0; i < attrs.getLength(); i++) {
909 Attr attr = (Attr) attrs.item(i);
910 b.append(String.format(" %s='%s'", attr.getName(),
911 UiBinderWriter.escapeAttributeText(attr.getValue())));
912 }
913 b.append(">");
914 return b.toString();
915 }
916
917 private JType getSafeHtmlType() {
918 if (safeHtmlType == null) {
919 safeHtmlType = oracle.findType(SafeHtml.class.getName());
920 }
921 return safeHtmlType;
922 }
923
924 private JType getStringType() {
925 if (stringType == null) {
926 stringType = oracle.findType(String.class.getCanonicalName());
927 }
928 return stringType;
929 }
930
931 private JClassType getUnitType() {
932 return oracle.findType(Unit.class.getCanonicalName()).isEnum();
933 }
934
935 public Element getElement() {
936 return elem;
937 }
938
939 }