1 /*
2  * Copyright (C) 2006 Joe Walnes.
3  * Copyright (C) 2006, 2007, 2008, 2009, 2011, 2013 XStream Committers.
4  * All rights reserved.
5  *
6  * The software in this package is published under the terms of the BSD
7  * style license a copy of which has been included with this distribution in
8  * the LICENSE.txt file.
9  *
10  * Created on 15. August 2009 by Joerg Schaible, copied from XmlFriendlyReplacer.
11  */

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 /**
23  * Encode and decode tag and attribute names in XML drivers.
24  * <p>
25  * This NameCoder is designed to ensure the correct encoding and decoding of names used for Java
26  * types and fields to XML tags and attribute names.
27  * </p>
28  * <p>
29  * The default replacements are:
30  * </p>
31  * <ul>
32  * <li><b>$</b> (dollar) chars are replaced with <b>_-</b> (underscore dash) string.</li>
33  * <li><b>_</b> (underscore) chars are replaced with <b>__</b> (double underscore) string.</li>
34  * <li>other characters that are invalid in XML names are encoded with <b>_.XXXX</b> (underscore
35  * dot followed by hex representation of character).</li>
36  * </ul>
37  * 
38  * @author J&ouml;rg Schaible
39  * @author Mauro Talevi
40  * @author Tatu Saloranta
41  * @author Michael Schnell
42  * @see <a href="http://www.w3.org/TR/REC-xml/#dt-name">XML 1.0 name definition</a>
43  * @see <a href="http://www.w3.org/TR/xml11/#dt-name">XML 1.1 name definition</a>
44  * @see <a href="http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.8">Java
45  *      identifier definition</a>
46  * @since 1.4
47  */

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         // legal characters in XML names according to
63         // http://www.w3.org/TR/REC-xml/#NT-Name and
64         // http://www.w3.org/TR/xml11/#NT-Name
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     /**
103      * Construct a new XmlFriendlyNameCoder.
104      * 
105      * @since 1.4
106      */

107     public XmlFriendlyNameCoder() {
108         this("_-""__");
109     }
110
111     /**
112      * Construct a new XmlFriendlyNameCoder with custom replacement strings for dollar and the
113      * escape character.
114      * 
115      * @param dollarReplacement
116      * @param escapeCharReplacement
117      * @since 1.4
118      */

119     public XmlFriendlyNameCoder(String dollarReplacement, String escapeCharReplacement) {
120         this(dollarReplacement, escapeCharReplacement, "_.");
121     }
122
123     /**
124      * Construct a new XmlFriendlyNameCoder with custom replacement strings for dollar, the
125      * escape character and the prefix for hexadecimal encoding of invalid characters in XML
126      * names.
127      * 
128      * @param dollarReplacement
129      * @param escapeCharReplacement
130      * @since 1.4
131      */

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     /**
141      * {@inheritDoc}
142      */

143     public String decodeAttribute(String attributeName) {
144         return decodeName(attributeName);
145     }
146
147     /**
148      * {@inheritDoc}
149      */

150     public String decodeNode(String elementName) {
151         return decodeName(elementName);
152     }
153
154     /**
155      * {@inheritDoc}
156      */

157     public String encodeAttribute(String name) {
158         return encodeName(name);
159     }
160
161     /**
162      * {@inheritDoc}
163      */

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             // First, fast (common) case: nothing to escape
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             // Otherwise full processing
188             final StringBuffer result = new StringBuffer(length + 8);
189
190             // We know first N chars are safe
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             // First, fast (common) case: nothing to decode
226             int i = 0;
227
228             for (; i < length; i++ ) {
229                 char c = name.charAt(i);
230                 // We'll do a quick check for potential match
231                 if (c == dollarReplacementFirstChar
232                     || c == escapeReplacementFirstChar
233                     || c == hexPrefixFirstChar) {
234                     // and if it might be a match, just quit, will check later on
235                     break;
236                 }
237             }
238
239             if (i == length) {
240                 return name;
241             }
242
243             // Otherwise full processing
244             final StringBuffer result = new StringBuffer(length + 8);
245
246             // We know first N chars are safe
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