1
12 package com.thoughtworks.xstream.io.xml;
13
14 import com.thoughtworks.xstream.converters.reflection.ObjectAccessException;
15 import com.thoughtworks.xstream.io.naming.NameCoder;
16
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.Map;
20
21
22
48 public class XmlFriendlyNameCoder implements NameCoder, Cloneable {
49 private static final IntPair[] XML_NAME_START_CHAR_BOUNDS;
50 private static final IntPair[] XML_NAME_CHAR_EXTRA_BOUNDS;
51 static {
52 class IntPairList extends ArrayList {
53 void add(int min, int max) {
54 super.add(new IntPair(min, max));
55 }
56
57 void add(char cp) {
58 super.add(new IntPair(cp, cp));
59 }
60 }
61
62
63
64
65 IntPairList list = new IntPairList();
66
67 list.add(':');
68 list.add('A', 'Z');
69 list.add('a', 'z');
70 list.add('_');
71
72 list.add(0xC0, 0xD6);
73 list.add(0xD8, 0xF6);
74 list.add(0xF8, 0x2FF);
75 list.add(0x370, 0x37D);
76 list.add(0x37F, 0x1FFF);
77 list.add(0x200C, 0x200D);
78 list.add(0x2070, 0x218F);
79 list.add(0x2C00, 0x2FEF);
80 list.add(0x3001, 0xD7FF);
81 list.add(0xF900, 0xFDCF);
82 list.add(0xFDF0, 0xFFFD);
83 list.add(0x10000, 0xEFFFF);
84 XML_NAME_START_CHAR_BOUNDS = (IntPair[])list.toArray(new IntPair[list.size()]);
85
86 list.clear();
87 list.add('-');
88 list.add('.');
89 list.add('0', '9');
90 list.add('\u00b7');
91 list.add(0x0300, 0x036F);
92 list.add(0x203F, 0x2040);
93 XML_NAME_CHAR_EXTRA_BOUNDS = (IntPair[])list.toArray(new IntPair[list.size()]);
94 }
95
96 private final String dollarReplacement;
97 private final String escapeCharReplacement;
98 private transient Map escapeCache;
99 private transient Map unescapeCache;
100 private final String hexPrefix;
101
102
107 public XmlFriendlyNameCoder() {
108 this("_-", "__");
109 }
110
111
119 public XmlFriendlyNameCoder(String dollarReplacement, String escapeCharReplacement) {
120 this(dollarReplacement, escapeCharReplacement, "_.");
121 }
122
123
132 public XmlFriendlyNameCoder(
133 String dollarReplacement, String escapeCharReplacement, String hexPrefix) {
134 this.dollarReplacement = dollarReplacement;
135 this.escapeCharReplacement = escapeCharReplacement;
136 this.hexPrefix = hexPrefix;
137 readResolve();
138 }
139
140
143 public String decodeAttribute(String attributeName) {
144 return decodeName(attributeName);
145 }
146
147
150 public String decodeNode(String elementName) {
151 return decodeName(elementName);
152 }
153
154
157 public String encodeAttribute(String name) {
158 return encodeName(name);
159 }
160
161
164 public String encodeNode(String name) {
165 return encodeName(name);
166 }
167
168 private String encodeName(String name) {
169 String s = (String)escapeCache.get(name);
170 if (s == null) {
171 final int length = name.length();
172
173
174 int i = 0;
175
176 for (; i < length; i++ ) {
177 char c = name.charAt(i);
178 if (c == '$' || c == '_' || c <= 27 || c >= 127) {
179 break;
180 }
181 }
182
183 if (i == length) {
184 return name;
185 }
186
187
188 final StringBuffer result = new StringBuffer(length + 8);
189
190
191 if (i > 0) {
192 result.append(name.substring(0, i));
193 }
194
195 for (; i < length; i++ ) {
196 char c = name.charAt(i);
197 if (c == '$') {
198 result.append(dollarReplacement);
199 } else if (c == '_') {
200 result.append(escapeCharReplacement);
201 } else if ((i == 0 && !isXmlNameStartChar(c)) || (i > 0 && !isXmlNameChar(c))) {
202 result.append(hexPrefix);
203 if (c < 16) result.append("000");
204 else if (c < 256) result.append("00");
205 else if (c < 4096) result.append("0");
206 result.append(Integer.toHexString(c));
207 } else {
208 result.append(c);
209 }
210 }
211 s = result.toString();
212 escapeCache.put(name, s);
213 }
214 return s;
215 }
216
217 private String decodeName(String name) {
218 String s = (String)unescapeCache.get(name);
219 if (s == null) {
220 final char dollarReplacementFirstChar = dollarReplacement.charAt(0);
221 final char escapeReplacementFirstChar = escapeCharReplacement.charAt(0);
222 final char hexPrefixFirstChar = hexPrefix.charAt(0);
223 final int length = name.length();
224
225
226 int i = 0;
227
228 for (; i < length; i++ ) {
229 char c = name.charAt(i);
230
231 if (c == dollarReplacementFirstChar
232 || c == escapeReplacementFirstChar
233 || c == hexPrefixFirstChar) {
234
235 break;
236 }
237 }
238
239 if (i == length) {
240 return name;
241 }
242
243
244 final StringBuffer result = new StringBuffer(length + 8);
245
246
247 if (i > 0) {
248 result.append(name.substring(0, i));
249 }
250
251 for (; i < length; i++ ) {
252 char c = name.charAt(i);
253 if (c == dollarReplacementFirstChar && name.startsWith(dollarReplacement, i)) {
254 i += dollarReplacement.length() - 1;
255 result.append('$');
256 } else if (c == hexPrefixFirstChar && name.startsWith(hexPrefix, i)) {
257 i += hexPrefix.length();
258 c = (char)Integer.parseInt(name.substring(i, i + 4), 16);
259 i += 3;
260 result.append(c);
261 } else if (c == escapeReplacementFirstChar
262 && name.startsWith(escapeCharReplacement, i)) {
263 i += escapeCharReplacement.length() - 1;
264 result.append('_');
265 } else {
266 result.append(c);
267 }
268 }
269
270 s = result.toString();
271 unescapeCache.put(name, s);
272 }
273 return s;
274 }
275
276 public Object clone() {
277 try {
278 XmlFriendlyNameCoder coder = (XmlFriendlyNameCoder)super.clone();
279 coder.readResolve();
280 return coder;
281
282 } catch (CloneNotSupportedException e) {
283 throw new ObjectAccessException("Cannot clone XmlFriendlyNameCoder", e);
284 }
285 }
286
287 private Object readResolve() {
288 escapeCache = createCacheMap();
289 unescapeCache = createCacheMap();
290 return this;
291 }
292
293 protected Map createCacheMap() {
294 return new HashMap();
295 }
296
297 private static class IntPair {
298 int min;
299 int max;
300
301 public IntPair(int min, int max) {
302 this.min = min;
303 this.max = max;
304 }
305 }
306
307 private static boolean isXmlNameStartChar(int cp) {
308 return isInNameCharBounds(cp, XML_NAME_START_CHAR_BOUNDS);
309 }
310
311 private static boolean isXmlNameChar(int cp) {
312 if (isXmlNameStartChar(cp)) {
313 return true;
314 }
315 return isInNameCharBounds(cp, XML_NAME_CHAR_EXTRA_BOUNDS);
316 }
317
318 private static boolean isInNameCharBounds(int cp, IntPair[] nameCharBounds) {
319 for (int i = 0; i < nameCharBounds.length; ++i) {
320 IntPair p = nameCharBounds[i];
321 if (cp >= p.min && cp <= p.max) {
322 return true;
323 }
324 }
325 return false;
326 }
327 }
328