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