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