/* * 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 parseStringListAttribute(XmlPullParser parser, String attributeName, List defaultValue) throws XmlPullParserException { String attributeValueString = parser.getAttributeValue(null /* namespace */, attributeName); if (attributeValueString == null) { return defaultValue; } StringTokenizer stringTokenizer = new StringTokenizer(attributeValueString, ",", false); ArrayList 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. * *

When called, the parser must be pointing at one of: *

    *
  • the END_TAG we are looking for
  • *
  • a TEXT
  • *
  • a START_TAG nested within the element that can be consumed
  • *
* 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); } } }