1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.util;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.SystemProperties;
22 import android.system.ErrnoException;
23 import android.system.Os;
24 
25 import com.android.internal.util.ArtBinaryXmlPullParser;
26 import com.android.internal.util.ArtBinaryXmlSerializer;
27 import com.android.internal.util.FastXmlSerializer;
28 import com.android.internal.util.XmlUtils;
29 import com.android.modules.utils.BinaryXmlSerializer;
30 import com.android.modules.utils.TypedXmlPullParser;
31 import com.android.modules.utils.TypedXmlSerializer;
32 
33 import libcore.util.XmlObjectFactory;
34 
35 import org.xml.sax.ContentHandler;
36 import org.xml.sax.InputSource;
37 import org.xml.sax.SAXException;
38 import org.xml.sax.XMLReader;
39 import org.xmlpull.v1.XmlPullParser;
40 import org.xmlpull.v1.XmlPullParserException;
41 import org.xmlpull.v1.XmlSerializer;
42 
43 import java.io.BufferedInputStream;
44 import java.io.FileInputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 import java.io.Reader;
49 import java.io.StringReader;
50 import java.io.UnsupportedEncodingException;
51 import java.nio.charset.StandardCharsets;
52 import java.util.Arrays;
53 
54 /**
55  * XML utility methods.
56  */
57 public class Xml {
Xml()58     private Xml() {}
59 
60     /**
61      * {@link org.xmlpull.v1.XmlPullParser} "relaxed" feature name.
62      *
63      * @see <a href="http://xmlpull.org/v1/doc/features.html#relaxed">
64      *  specification</a>
65      */
66     public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed";
67 
68     /**
69      * Feature flag: when set, {@link #resolveSerializer(OutputStream)} will
70      * emit binary XML by default.
71      *
72      * @hide
73      */
74     public static final boolean ENABLE_BINARY_DEFAULT = SystemProperties
75             .getBoolean("persist.sys.binary_xml", true);
76 
77     /**
78      * Parses the given xml string and fires events on the given SAX handler.
79      */
parse(String xml, ContentHandler contentHandler)80     public static void parse(String xml, ContentHandler contentHandler)
81             throws SAXException {
82         try {
83             XMLReader reader = XmlObjectFactory.newXMLReader();
84             reader.setContentHandler(contentHandler);
85             reader.parse(new InputSource(new StringReader(xml)));
86         } catch (IOException e) {
87             throw new AssertionError(e);
88         }
89     }
90 
91     /**
92      * Parses xml from the given reader and fires events on the given SAX
93      * handler.
94      */
parse(Reader in, ContentHandler contentHandler)95     public static void parse(Reader in, ContentHandler contentHandler)
96             throws IOException, SAXException {
97         XMLReader reader = XmlObjectFactory.newXMLReader();
98         reader.setContentHandler(contentHandler);
99         reader.parse(new InputSource(in));
100     }
101 
102     /**
103      * Parses xml from the given input stream and fires events on the given SAX
104      * handler.
105      */
parse(InputStream in, Encoding encoding, ContentHandler contentHandler)106     public static void parse(InputStream in, Encoding encoding,
107             ContentHandler contentHandler) throws IOException, SAXException {
108         XMLReader reader = XmlObjectFactory.newXMLReader();
109         reader.setContentHandler(contentHandler);
110         InputSource source = new InputSource(in);
111         source.setEncoding(encoding.expatName);
112         reader.parse(source);
113     }
114 
115     /**
116      * Returns a new pull parser with namespace support.
117      */
newPullParser()118     public static XmlPullParser newPullParser() {
119         try {
120             XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
121             parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
122             parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
123             return parser;
124         } catch (XmlPullParserException e) {
125             throw new AssertionError();
126         }
127     }
128 
129     /**
130      * Creates a new {@link TypedXmlPullParser} which is optimized for use
131      * inside the system, typically by supporting only a basic set of features.
132      * <p>
133      * In particular, the returned parser does not support namespaces, prefixes,
134      * properties, or options.
135      *
136      * @hide
137      */
138     @SuppressWarnings("AndroidFrameworkEfficientXml")
newFastPullParser()139     public static @NonNull TypedXmlPullParser newFastPullParser() {
140         return XmlUtils.makeTyped(newPullParser());
141     }
142 
143     /**
144      * Creates a new {@link XmlPullParser} that reads XML documents using a
145      * custom binary wire protocol which benchmarking has shown to be 8.5x
146      * faster than {@code Xml.newFastPullParser()} for a typical
147      * {@code packages.xml}.
148      *
149      * @hide
150      */
newBinaryPullParser()151     public static @NonNull TypedXmlPullParser newBinaryPullParser() {
152         return new ArtBinaryXmlPullParser();
153     }
154 
155     /**
156      * Creates a new {@link XmlPullParser} which is optimized for use inside the
157      * system, typically by supporting only a basic set of features.
158      * <p>
159      * This returned instance may be configured to read using an efficient
160      * binary format instead of a human-readable text format, depending on
161      * device feature flags.
162      * <p>
163      * To ensure that both formats are detected and transparently handled
164      * correctly, you must shift to using both {@link #resolveSerializer} and
165      * {@link #resolvePullParser}.
166      *
167      * @hide
168      */
resolvePullParser(@onNull InputStream in)169     public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
170             throws IOException {
171         final byte[] magic = new byte[4];
172         if (in instanceof FileInputStream) {
173             try {
174                 Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
175             } catch (ErrnoException e) {
176                 throw e.rethrowAsIOException();
177             }
178         } else {
179             if (!in.markSupported()) {
180                 in = new BufferedInputStream(in);
181             }
182             in.mark(8);
183             in.read(magic);
184             in.reset();
185         }
186 
187         final TypedXmlPullParser xml;
188         if (Arrays.equals(magic, BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0)) {
189             xml = newBinaryPullParser();
190         } else {
191             xml = newFastPullParser();
192         }
193         try {
194             xml.setInput(in, StandardCharsets.UTF_8.name());
195         } catch (XmlPullParserException e) {
196             throw new IOException(e);
197         }
198         return xml;
199     }
200 
201     /**
202      * Creates a new xml serializer.
203      */
newSerializer()204     public static XmlSerializer newSerializer() {
205         return XmlObjectFactory.newXmlSerializer();
206     }
207 
208     /**
209      * Creates a new {@link XmlSerializer} which is optimized for use inside the
210      * system, typically by supporting only a basic set of features.
211      * <p>
212      * In particular, the returned parser does not support namespaces, prefixes,
213      * properties, or options.
214      *
215      * @hide
216      */
217     @SuppressWarnings("AndroidFrameworkEfficientXml")
newFastSerializer()218     public static @NonNull TypedXmlSerializer newFastSerializer() {
219         return XmlUtils.makeTyped(new FastXmlSerializer());
220     }
221 
222     /**
223      * Creates a new {@link XmlSerializer} that writes XML documents using a
224      * custom binary wire protocol which benchmarking has shown to be 4.4x
225      * faster and use 2.8x less disk space than {@code Xml.newFastSerializer()}
226      * for a typical {@code packages.xml}.
227      *
228      * @hide
229      */
newBinarySerializer()230     public static @NonNull TypedXmlSerializer newBinarySerializer() {
231         return new ArtBinaryXmlSerializer();
232     }
233 
234     /**
235      * Creates a new {@link XmlSerializer} which is optimized for use inside the
236      * system, typically by supporting only a basic set of features.
237      * <p>
238      * This returned instance may be configured to write using an efficient
239      * binary format instead of a human-readable text format, depending on
240      * device feature flags.
241      * <p>
242      * To ensure that both formats are detected and transparently handled
243      * correctly, you must shift to using both {@link #resolveSerializer} and
244      * {@link #resolvePullParser}.
245      *
246      * @hide
247      */
resolveSerializer(@onNull OutputStream out)248     public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
249             throws IOException {
250         final TypedXmlSerializer xml;
251         if (ENABLE_BINARY_DEFAULT) {
252             xml = newBinarySerializer();
253         } else {
254             xml = newFastSerializer();
255         }
256         xml.setOutput(out, StandardCharsets.UTF_8.name());
257         return xml;
258     }
259 
260     /**
261      * Copy the first XML document into the second document.
262      * <p>
263      * Implemented by reading all events from the given {@link XmlPullParser}
264      * and writing them directly to the given {@link XmlSerializer}. This can be
265      * useful for transparently converting between underlying wire protocols.
266      *
267      * @hide
268      */
copy(@onNull XmlPullParser in, @NonNull XmlSerializer out)269     public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
270             throws XmlPullParserException, IOException {
271         // Some parsers may have already consumed the event that starts the
272         // document, so we manually emit that event here for consistency
273         if (in.getEventType() == XmlPullParser.START_DOCUMENT) {
274             out.startDocument(in.getInputEncoding(), true);
275         }
276 
277         while (true) {
278             final int token = in.nextToken();
279             switch (token) {
280                 case XmlPullParser.START_DOCUMENT:
281                     out.startDocument(in.getInputEncoding(), true);
282                     break;
283                 case XmlPullParser.END_DOCUMENT:
284                     out.endDocument();
285                     return;
286                 case XmlPullParser.START_TAG:
287                     out.startTag(normalizeNamespace(in.getNamespace()), in.getName());
288                     for (int i = 0; i < in.getAttributeCount(); i++) {
289                         out.attribute(normalizeNamespace(in.getAttributeNamespace(i)),
290                                 in.getAttributeName(i), in.getAttributeValue(i));
291                     }
292                     break;
293                 case XmlPullParser.END_TAG:
294                     out.endTag(normalizeNamespace(in.getNamespace()), in.getName());
295                     break;
296                 case XmlPullParser.TEXT:
297                     out.text(in.getText());
298                     break;
299                 case XmlPullParser.CDSECT:
300                     out.cdsect(in.getText());
301                     break;
302                 case XmlPullParser.ENTITY_REF:
303                     out.entityRef(in.getName());
304                     break;
305                 case XmlPullParser.IGNORABLE_WHITESPACE:
306                     out.ignorableWhitespace(in.getText());
307                     break;
308                 case XmlPullParser.PROCESSING_INSTRUCTION:
309                     out.processingInstruction(in.getText());
310                     break;
311                 case XmlPullParser.COMMENT:
312                     out.comment(in.getText());
313                     break;
314                 case XmlPullParser.DOCDECL:
315                     out.docdecl(in.getText());
316                     break;
317                 default:
318                     throw new IllegalStateException("Unknown token " + token);
319             }
320         }
321     }
322 
323     /**
324      * Some parsers may return an empty string {@code ""} when a namespace in
325      * unsupported, which can confuse serializers. This method normalizes empty
326      * strings to be {@code null}.
327      */
normalizeNamespace(@ullable String namespace)328     private static @Nullable String normalizeNamespace(@Nullable String namespace) {
329         if (namespace == null || namespace.isEmpty()) {
330             return null;
331         } else {
332             return namespace;
333         }
334     }
335 
336     /**
337      * Supported character encodings.
338      */
339     public enum Encoding {
340 
341         US_ASCII("US-ASCII"),
342         UTF_8("UTF-8"),
343         UTF_16("UTF-16"),
344         ISO_8859_1("ISO-8859-1");
345 
346         final String expatName;
347 
Encoding(String expatName)348         Encoding(String expatName) {
349             this.expatName = expatName;
350         }
351     }
352 
353     /**
354      * Finds an encoding by name. Returns UTF-8 if you pass {@code null}.
355      */
findEncodingByName(String encodingName)356     public static Encoding findEncodingByName(String encodingName)
357             throws UnsupportedEncodingException {
358         if (encodingName == null) {
359             return Encoding.UTF_8;
360         }
361 
362         for (Encoding encoding : Encoding.values()) {
363             if (encoding.expatName.equalsIgnoreCase(encodingName))
364                 return encoding;
365         }
366         throw new UnsupportedEncodingException(encodingName);
367     }
368 
369     /**
370      * Return an AttributeSet interface for use with the given XmlPullParser.
371      * If the given parser itself implements AttributeSet, that implementation
372      * is simply returned.  Otherwise a wrapper class is
373      * instantiated on top of the XmlPullParser, as a proxy for retrieving its
374      * attributes, and returned to you.
375      *
376      * @param parser The existing parser for which you would like an
377      *               AttributeSet.
378      *
379      * @return An AttributeSet you can use to retrieve the
380      *         attribute values at each of the tags as the parser moves
381      *         through its XML document.
382      *
383      * @see AttributeSet
384      */
asAttributeSet(XmlPullParser parser)385     public static AttributeSet asAttributeSet(XmlPullParser parser) {
386         return (parser instanceof AttributeSet)
387                 ? (AttributeSet) parser
388                 : new XmlPullAttributes(parser);
389     }
390 }
391