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