View Javadoc
1   package com.github.davidmoten.aws.lw.client.xml.builder;
2   
3   import java.util.ArrayList;
4   import java.util.HashMap;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.stream.Collectors;
8   
9   import com.github.davidmoten.aws.lw.client.internal.util.Preconditions;
10  
11  public final class Xml {
12  
13      private final String name;
14      private final Xml parent;
15      private Map<String, String> attributes = new HashMap<>();
16      private List<Xml> children = new ArrayList<>();
17      private String content;
18      private boolean prelude = true;
19  
20      private Xml(String name) {
21          this(name, null);
22      }
23  
24      private Xml(String name, Xml parent) {
25          checkPresent(name, "name");
26          this.name = name;
27          this.parent = parent;
28      }
29  
30      public static Xml create(String name) {
31          return new Xml(name);
32      }
33  
34      public Xml excludePrelude() {
35          Xml xml = this;
36          while (xml.parent != null) {
37              xml = xml.parent;
38          }
39          xml.prelude = false;
40          return this;
41      }
42  
43      public Xml element(String name) {
44          checkPresent(name, "name");
45          Preconditions.checkArgument(content == null,
46                  "content cannot be already specified if starting a child element");
47          Xml xml = new Xml(name, this);
48          this.children.add(xml);
49          return xml;
50      }
51  
52      public Xml e(String name) {
53          return element(name);
54      }
55  
56      public Xml attribute(String name, String value) {
57          checkPresent(name, "name");
58          Preconditions.checkNotNull(value);
59          this.attributes.put(name, value);
60          return this;
61      }
62  
63      public Xml a(String name, String value) {
64          return attribute(name, value);
65      }
66  
67      public Xml content(String content) {
68          Preconditions.checkArgument(children.isEmpty());
69          this.content = content;
70          return this;
71      }
72  
73      public Xml up() {
74          return parent;
75      }
76  
77      private static void checkPresent(String s, String name) {
78          if (s == null || s.trim().isEmpty()) {
79              throw new IllegalArgumentException(name + " must be non-null and non-blank");
80          }
81      }
82  
83      private String toString(String indent) {
84          StringBuilder b = new StringBuilder();
85          if (indent.length() == 0 && prelude) {
86              b.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
87          }
88          // TODO encode attributes and content for xml
89          String atts = attributes.entrySet().stream().map(
90                  entry -> " " + entry.getKey() + "=\"" + encodeXml(entry.getValue(), true) + "\"")
91                  .collect(Collectors.joining());
92          b.append(String.format("%s<%s%s>", indent, name, atts));
93          if (content != null) {
94              b.append(encodeXml(content, false));
95              b.append(String.format("</%s>", name));
96              if (parent != null) {
97                  b.append("\n");
98              }
99          } else {
100             b.append("\n");
101             for (Xml xml : children) {
102                 b.append(xml.toString(indent + "  "));
103             }
104             b.append(String.format("%s</%s>", indent, name));
105             if (parent != null) {
106                 b.append("\n");
107             }
108         }
109         return b.toString();
110     }
111 
112     public String toString() {
113         Xml xml = this;
114         while (xml.parent != null) {
115             xml = xml.parent;
116         }
117         return xml.toString("");
118     }
119 
120     private static final Map<Integer, String> CONTENT_CHARACTER_MAP = createContentCharacterMap();
121     private static final Map<Integer, String> ATTRIBUTE_CHARACTER_MAP = createAttributeCharacterMap();
122 
123     private static Map<Integer, String> createContentCharacterMap() {
124         Map<Integer, String> m = new HashMap<>();
125         m.put((int) '&', "&amp;");
126         m.put((int) '>', "&gt;");
127         m.put((int) '<', "&lt;");
128         return m;
129     }
130 
131     private static Map<Integer, String> createAttributeCharacterMap() {
132         Map<Integer, String> m = new HashMap<>();
133         m.put((int) '\'', "&apos;");
134         m.put((int) '\"', "&quot;");
135         return m;
136     }
137 
138     private static String encodeXml(CharSequence s, boolean isAttribute) {
139         StringBuilder b = new StringBuilder();
140         int len = s.length();
141         for (int i = 0; i < len; i++) {
142             int c = s.charAt(i);
143             if (c >= 0xd800 && c <= 0xdbff && i + 1 < len) {
144                 c = ((c - 0xd7c0) << 10) | (s.charAt(++i) & 0x3ff); // UTF16 decode
145             }
146             if (c < 0x80) { // ASCII range: test most common case first
147                 if (c < 0x20 && (c != '\t' && c != '\r' && c != '\n')) {
148                     // Illegal XML character, even encoded. Skip or substitute
149                     b.append("&#xfffd;"); // Unicode replacement character
150                 } else {
151                     String r = CONTENT_CHARACTER_MAP.get(c);
152                     if (r != null) {
153                         b.append(r);
154                     } else if (isAttribute) {
155                         String r2 = ATTRIBUTE_CHARACTER_MAP.get(c);
156                         if (r2 != null) {
157                             b.append(r2);
158                         } else {
159                             b.append((char) c);
160                         }
161                     } else {
162                         b.append((char) c);
163                     }
164 
165                 }
166             } else if ((c >= 0xd800 && c <= 0xdfff) || c == 0xfffe || c == 0xffff) {
167                 // Illegal XML character, even encoded. Skip or substitute
168                 b.append("&#xfffd;"); // Unicode replacement character
169             } else {
170                 b.append("&#x");
171                 b.append(Integer.toHexString(c));
172                 b.append(';');
173             }
174         }
175         return b.toString();
176     }
177 
178 }