blob: 5fb67313e6d117394519b4f113c0b142bc626144 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.pm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Slog;
import android.util.Xml;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Stack;
/**
* A very specialized serialization/parsing wrapper around {@link TypedXmlSerializer} and {@link
* TypedXmlPullParser} intended for use with PackageManager related settings files.
* Assumptions/chosen behaviors:
* <ul>
* <li>No namespace support</li>
* <li>Data for a parent object is stored as attributes</li>
* <li>All attribute read methods return a default false, -1, or null</li>
* <li>Default values will not be written</li>
* <li>Children are sub-elements</li>
* <li>Collections are repeated sub-elements, no attribute support for collections</li>
* </ul>
*/
public class SettingsXml {
private static final String TAG = "SettingsXml";
private static final boolean DEBUG_THROW_EXCEPTIONS = false;
private static final String FEATURE_INDENT =
"http://xmlpull.org/v1/doc/features.html#indent-output";
private static final int DEFAULT_NUMBER = -1;
public static Serializer serializer(TypedXmlSerializer serializer) {
return new Serializer(serializer);
}
public static ReadSection parser(TypedXmlPullParser parser)
throws IOException, XmlPullParserException {
return new ReadSectionImpl(parser);
}
public static class Serializer implements AutoCloseable {
@NonNull
private final TypedXmlSerializer mXmlSerializer;
private final WriteSectionImpl mWriteSection;
private Serializer(TypedXmlSerializer serializer) {
mXmlSerializer = serializer;
mWriteSection = new WriteSectionImpl(mXmlSerializer);
}
public WriteSection startSection(@NonNull String sectionName) throws IOException {
return mWriteSection.startSection(sectionName);
}
@Override
public void close() throws IOException {
mWriteSection.closeCompletely();
mXmlSerializer.flush();
}
}
public interface ReadSection extends AutoCloseable {
@NonNull
String getName();
@NonNull
String getDescription();
boolean has(String attrName);
@Nullable
String getString(String attrName);
/**
* @return value as String or {@param defaultValue} if doesn't exist
*/
@NonNull
String getString(String attrName, @NonNull String defaultValue);
/**
* @return value as boolean or false if doesn't exist
*/
boolean getBoolean(String attrName);
/**
* @return value as boolean or {@param defaultValue} if doesn't exist
*/
boolean getBoolean(String attrName, boolean defaultValue);
/**
* @return value as int or {@link #DEFAULT_NUMBER} if doesn't exist
*/
int getInt(String attrName);
/**
* @return value as int or {@param defaultValue} if doesn't exist
*/
int getInt(String attrName, int defaultValue);
/**
* @return value as long or {@link #DEFAULT_NUMBER} if doesn't exist
*/
long getLong(String attrName);
/**
* @return value as long or {@param defaultValue} if doesn't exist
*/
long getLong(String attrName, int defaultValue);
ChildSection children();
}
/**
* <pre><code>
* ChildSection child = parentSection.children();
* while (child.moveToNext(TAG_CHILD)) {
* String readValue = child.getString(...);
* ...
* }
* </code></pre>
*/
public interface ChildSection extends ReadSection {
boolean moveToNext();
boolean moveToNext(@NonNull String expectedChildTagName);
}
public static class ReadSectionImpl implements ChildSection {
@Nullable
private final InputStream mInput;
@NonNull
private final TypedXmlPullParser mParser;
@NonNull
private final Stack<Integer> mDepthStack = new Stack<>();
public ReadSectionImpl(@NonNull InputStream input)
throws IOException, XmlPullParserException {
mInput = input;
mParser = Xml.newFastPullParser();
mParser.setInput(mInput, StandardCharsets.UTF_8.name());
moveToFirstTag();
}
public ReadSectionImpl(@NonNull TypedXmlPullParser parser)
throws IOException, XmlPullParserException {
mInput = null;
mParser = parser;
moveToFirstTag();
}
private void moveToFirstTag() throws IOException, XmlPullParserException {
if (mParser.getEventType() == XmlPullParser.START_TAG) {
return;
}
int type;
//noinspection StatementWithEmptyBody
while ((type = mParser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
}
}
@NonNull
@Override
public String getName() {
return mParser.getName();
}
@NonNull
@Override
public String getDescription() {
return mParser.getPositionDescription();
}
@Override
public boolean has(String attrName) {
return mParser.getAttributeValue(null, attrName) != null;
}
@Nullable
@Override
public String getString(String attrName) {
return mParser.getAttributeValue(null, attrName);
}
@NonNull
@Override
public String getString(String attrName, @NonNull String defaultValue) {
String value = mParser.getAttributeValue(null, attrName);
if (value == null) {
return defaultValue;
}
return value;
}
@Override
public boolean getBoolean(String attrName) {
return getBoolean(attrName, false);
}
@Override
public boolean getBoolean(String attrName, boolean defaultValue) {
return mParser.getAttributeBoolean(null, attrName, defaultValue);
}
@Override
public int getInt(String attrName) {
return getInt(attrName, DEFAULT_NUMBER);
}
@Override
public int getInt(String attrName, int defaultValue) {
return mParser.getAttributeInt(null, attrName, defaultValue);
}
@Override
public long getLong(String attrName) {
return getLong(attrName, DEFAULT_NUMBER);
}
@Override
public long getLong(String attrName, int defaultValue) {
return mParser.getAttributeLong(null, attrName, defaultValue);
}
@Override
public ChildSection children() {
mDepthStack.push(mParser.getDepth());
return this;
}
@Override
public boolean moveToNext() {
return moveToNextInternal(null);
}
@Override
public boolean moveToNext(@NonNull String expectedChildTagName) {
return moveToNextInternal(expectedChildTagName);
}
private boolean moveToNextInternal(@Nullable String expectedChildTagName) {
try {
int depth = mDepthStack.peek();
boolean hasTag = false;
int type;
while (!hasTag
&& (type = mParser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || mParser.getDepth() > depth)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (expectedChildTagName != null
&& !expectedChildTagName.equals(mParser.getName())) {
continue;
}
hasTag = true;
}
if (!hasTag) {
mDepthStack.pop();
}
return hasTag;
} catch (Exception ignored) {
return false;
}
}
@Override
public void close() throws Exception {
if (mDepthStack.isEmpty()) {
Slog.wtf(TAG, "Children depth stack was not empty, data may have been lost",
new Exception());
}
if (mInput != null) {
mInput.close();
}
}
}
public interface WriteSection extends AutoCloseable {
WriteSection startSection(@NonNull String sectionName) throws IOException;
WriteSection attribute(String attrName, @Nullable String value) throws IOException;
WriteSection attribute(String attrName, int value) throws IOException;
WriteSection attribute(String attrName, long value) throws IOException;
WriteSection attribute(String attrName, boolean value) throws IOException;
@Override
void close() throws IOException;
void finish() throws IOException;
}
private static class WriteSectionImpl implements WriteSection {
@NonNull
private final TypedXmlSerializer mXmlSerializer;
@NonNull
private final Stack<String> mTagStack = new Stack<>();
private WriteSectionImpl(@NonNull TypedXmlSerializer xmlSerializer) {
mXmlSerializer = xmlSerializer;
}
@Override
public WriteSection startSection(@NonNull String sectionName) throws IOException {
// Try to start the tag first before we push it to the stack
mXmlSerializer.startTag(null, sectionName);
mTagStack.push(sectionName);
return this;
}
@Override
public WriteSection attribute(String attrName, String value) throws IOException {
if (value != null) {
mXmlSerializer.attribute(null, attrName, value);
}
return this;
}
@Override
public WriteSection attribute(String attrName, int value) throws IOException {
if (value != DEFAULT_NUMBER) {
mXmlSerializer.attributeInt(null, attrName, value);
}
return this;
}
@Override
public WriteSection attribute(String attrName, long value) throws IOException {
if (value != DEFAULT_NUMBER) {
mXmlSerializer.attributeLong(null, attrName, value);
}
return this;
}
@Override
public WriteSection attribute(String attrName, boolean value) throws IOException {
if (value) {
mXmlSerializer.attributeBoolean(null, attrName, value);
}
return this;
}
@Override
public void finish() throws IOException {
close();
}
@Override
public void close() throws IOException {
mXmlSerializer.endTag(null, mTagStack.pop());
}
private void closeCompletely() throws IOException {
if (DEBUG_THROW_EXCEPTIONS && mTagStack != null && !mTagStack.isEmpty()) {
throw new IllegalStateException(
"tag stack is not empty when closing, contains " + mTagStack);
} else if (mTagStack != null) {
while (!mTagStack.isEmpty()) {
close();
}
}
}
}
}