blob: fc2535d49038788d0a9b6c8795e8ee474cf38272 [file] [log] [blame]
/*
* Copyright (C) 2019 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.i18n.timezone;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
class XmlUtils {
private static final String TRUE_ATTRIBUTE_VALUE = "y";
private static final String FALSE_ATTRIBUTE_VALUE = "n";
private XmlUtils() {}
/**
* Parses an attribute value, which must be either {@code null} or a valid signed long value.
* If the attribute value is {@code null} then {@code defaultValue} is returned. If the
* attribute is present but not a valid long value then an XmlPullParserException is thrown.
*/
static Long parseLongAttribute(XmlPullParser parser, String attributeName,
Long defaultValue) throws XmlPullParserException {
String attributeValueString = parser.getAttributeValue(null /* namespace */, attributeName);
if (attributeValueString == null) {
return defaultValue;
}
try {
return Long.parseLong(attributeValueString);
} catch (NumberFormatException e) {
throw new XmlPullParserException("Attribute \"" + attributeName
+ "\" is not a long value: " + parser.getPositionDescription());
}
}
/**
* Parses an attribute value, which must be either {@code null}, {@code "y"} or {@code "n"}.
* If the attribute value is {@code null} then {@code defaultValue} is returned. If the
* attribute is present but not "y" or "n" then an XmlPullParserException is thrown.
*/
static Boolean parseBooleanAttribute(XmlPullParser parser,
String attributeName, Boolean defaultValue) throws XmlPullParserException {
String attributeValueString = parser.getAttributeValue(null /* namespace */, attributeName);
if (attributeValueString == null) {
return defaultValue;
}
boolean isTrue = TRUE_ATTRIBUTE_VALUE.equals(attributeValueString);
if (!(isTrue || FALSE_ATTRIBUTE_VALUE.equals(attributeValueString))) {
throw new XmlPullParserException("Attribute \"" + attributeName
+ "\" is not \"y\" or \"n\": " + parser.getPositionDescription());
}
return isTrue;
}
/**
* Parses an attribute value, which must be either {@code null} or a comma-separated String
* list. There is no support for escaping the comma. If the attribute value is {@code null} then
* {@code defaultValue} is returned.
*/
static List<String> parseStringListAttribute(XmlPullParser parser, String attributeName,
List<String> defaultValue) throws XmlPullParserException {
String attributeValueString = parser.getAttributeValue(null /* namespace */, attributeName);
if (attributeValueString == null) {
return defaultValue;
}
StringTokenizer stringTokenizer = new StringTokenizer(attributeValueString, ",", false);
ArrayList<String> strings = new ArrayList<>();
while (stringTokenizer.hasMoreTokens()) {
strings.add(stringTokenizer.nextToken());
}
strings.trimToSize();
return strings;
}
/**
* Advances the the parser to the START_TAG for the specified element without decreasing the
* depth, or increasing the depth by more than one (i.e. no recursion into child nodes).
* If the next (non-nested) END_TAG an exception is thrown. Throws an exception if the end of
* the document is encountered unexpectedly.
*/
static void findNextStartTagOrThrowNoRecurse(XmlPullParser parser, String elementName)
throws IOException, XmlPullParserException {
if (!findNextStartTagOrEndTagNoRecurse(parser, elementName)) {
throw new XmlPullParserException("No next element found with name " + elementName);
}
}
/**
* Advances the the parser to the START_TAG for the specified element without decreasing the
* depth, or increasing the depth by more than one (i.e. no recursion into child nodes).
* Returns {@code true} if the requested START_TAG is found, or {@code false} when the next
* (non-nested) END_TAG is encountered instead. Throws an exception if the end of the document
* is encountered unexpectedly.
*/
static boolean findNextStartTagOrEndTagNoRecurse(XmlPullParser parser, String elementName)
throws IOException, XmlPullParserException {
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
switch (type) {
case XmlPullParser.START_TAG:
String currentElementName = parser.getName();
if (elementName.equals(currentElementName)) {
return true;
}
// It was not the START_TAG we were looking for. Consume until the end.
parser.next();
consumeUntilEndTag(parser, currentElementName);
break;
case XmlPullParser.END_TAG:
return false;
default:
// Ignore.
break;
}
}
throw new XmlPullParserException("Unexpected end of document while looking for "
+ elementName);
}
/**
* Consume any remaining contents of an element and move to the END_TAG. Used when processing
* within an element can stop.
*
* <p>When called, the parser must be pointing at one of:
* <ul>
* <li>the END_TAG we are looking for</li>
* <li>a TEXT</li>
* <li>a START_TAG nested within the element that can be consumed</li>
* </ul>
* Note: The parser synthesizes an END_TAG for self-closing tags so this works for them too.
*/
static void consumeUntilEndTag(XmlPullParser parser, String elementName)
throws IOException, XmlPullParserException {
if (isEndTag(parser, elementName)) {
// Early return - we are already there.
return;
}
// Keep track of the required depth in case there are nested elements to be consumed.
// Both the name and the depth must match our expectation to complete.
int requiredDepth = parser.getDepth();
// A TEXT tag would be at the same depth as the END_TAG we are looking for.
if (parser.getEventType() == XmlPullParser.START_TAG) {
// A START_TAG would have incremented the depth, so we're looking for an END_TAG one
// higher than the current tag.
requiredDepth--;
}
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
int type = parser.next();
int currentDepth = parser.getDepth();
if (currentDepth < requiredDepth) {
throw new XmlPullParserException(
"Unexpected depth while looking for end tag: "
+ parser.getPositionDescription());
} else if (currentDepth == requiredDepth) {
if (type == XmlPullParser.END_TAG) {
if (elementName.equals(parser.getName())) {
return;
}
throw new XmlPullParserException(
"Unexpected eng tag: " + parser.getPositionDescription());
}
}
// Everything else is either a type we are not interested in or is too deep and so is
// ignored.
}
throw new XmlPullParserException("Unexpected end of document");
}
/**
* Throws an exception if the current element is not an end tag.
* Note: The parser synthesizes an END_TAG for self-closing tags so this works for them too.
*/
static void checkOnEndTag(XmlPullParser parser, String elementName)
throws XmlPullParserException {
if (!isEndTag(parser, elementName)) {
throw new XmlPullParserException(
"Unexpected tag encountered: " + parser.getPositionDescription());
}
}
/**
* Returns true if the current tag is an end tag.
* Note: The parser synthesizes an END_TAG for self-closing tags so this works for them too.
*/
private static boolean isEndTag(XmlPullParser parser, String elementName)
throws XmlPullParserException {
return parser.getEventType() == XmlPullParser.END_TAG
&& parser.getName().equals(elementName);
}
static String normalizeCountryIso(String countryIso) {
// Lowercase ASCII is normalized for the purposes of the input files and the code in this
// class and related classes.
return countryIso.toLowerCase(Locale.US);
}
/**
* Reads the text inside the current element. Should be called when the parser is currently
* on the START_TAG before the TEXT. The parser will be positioned on the END_TAG after this
* call when it completes successfully.
*/
static String consumeText(XmlPullParser parser)
throws IOException, XmlPullParserException {
int type = parser.next();
String text;
if (type == XmlPullParser.TEXT) {
text = parser.getText();
} else {
throw new XmlPullParserException("Text not found. Found type=" + type
+ " at " + parser.getPositionDescription());
}
type = parser.next();
if (type != XmlPullParser.END_TAG) {
throw new XmlPullParserException(
"Unexpected nested tag or end of document when expecting text: type=" + type
+ " at " + parser.getPositionDescription());
}
return text;
}
/**
* A source of Readers that can be used repeatedly.
*/
interface ReaderSupplier {
/** Returns a Reader. Throws an IOException if the Reader cannot be created. */
Reader get() throws IOException;
static ReaderSupplier forFile(String fileName, Charset charSet) throws IOException {
Path file = Paths.get(fileName);
if (!Files.exists(file)) {
throw new FileNotFoundException(fileName + " does not exist");
}
if (!Files.isRegularFile(file) && Files.isReadable(file)) {
throw new IOException(fileName + " must be a regular readable file.");
}
return () -> Files.newBufferedReader(file, charSet);
}
static ReaderSupplier forString(String xml) {
return () -> new StringReader(xml);
}
}
}