blob: b5388727708c0d2e0afe95580341f7fca0d32087 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.dvlib;
import com.android.annotations.Nullable;
import com.android.io.NonClosingInputStream;
import com.android.io.NonClosingInputStream.CloseBehavior;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
public class DeviceSchema {
// ---- XSD ----
/**
* The latest version of the device XML Schema.
* Valid version numbers are between 1 and this number, included.
*/
public static final int NS_LATEST_VERSION = 2;
/** The XML namespace of the latest device XML. */
public static final String NS_DEVICES_URI = getSchemaUri(NS_LATEST_VERSION);
/** Base for the devices XSD URI, without the terminal version number. */
private static final String NS_DEVICES_URI_BASE = "http://schemas.android.com/sdk/devices/";
/** Regex pattern to find the terminal version number in an XSD URI. */
static final String NS_DEVICES_URI_PATTERN = NS_DEVICES_URI_BASE + "([0-9]+)"; //$NON-NLS-1$
// ----- XML ----
/**
* The "devices" element is the root element of this schema.
*
* It must contain one or more "device" elements that each define the
* hardware, software, and states for a given device.
*/
public static final String NODE_DEVICES = "devices";
/**
* A "device" element contains a "hardware" element, a "software" element
* for each API version it supports, and a "state" element for each possible
* state the device could be in.
*/
public static final String NODE_DEVICE = "device";
/**
* The "hardware" element contains all of the hardware information for a
* given device.
*/
public static final String NODE_HARDWARE = "hardware";
/**
* The "software" element contains all of the software information for an
* API version of the given device.
*/
public static final String NODE_SOFTWARE = "software";
/**
* The "state" element contains all of the parameters for a given state of
* the device. It's also capable of redefining hardware configurations if
* they change based on state.
*/
public static final String NODE_STATE = "state";
public static final String NODE_KEYBOARD = "keyboard";
public static final String NODE_TOUCH = "touch";
public static final String NODE_GL_EXTENSIONS = "gl-extensions";
public static final String NODE_GL_VERSION = "gl-version";
public static final String NODE_NETWORKING = "networking";
public static final String NODE_REMOVABLE_STORAGE = "removable-storage";
public static final String NODE_FLASH = "flash";
public static final String NODE_LIVE_WALLPAPER_SUPPORT = "live-wallpaper-support";
public static final String NODE_STATUS_BAR = "status-bar";
public static final String NODE_BUTTONS = "buttons";
public static final String NODE_CAMERA = "camera";
public static final String NODE_LOCATION = "location";
public static final String NODE_GPU = "gpu";
public static final String NODE_DOCK = "dock";
public static final String NODE_YDPI = "ydpi";
public static final String NODE_POWER_TYPE= "power-type";
public static final String NODE_Y_DIMENSION = "y-dimension";
public static final String NODE_SCREEN_RATIO = "screen-ratio";
public static final String NODE_NAV_STATE = "nav-state";
public static final String NODE_MIC = "mic";
public static final String NODE_RAM = "ram";
public static final String NODE_XDPI = "xdpi";
public static final String NODE_DIMENSIONS = "dimensions";
public static final String NODE_ABI = "abi";
public static final String NODE_MECHANISM = "mechanism";
public static final String NODE_MULTITOUCH = "multitouch";
public static final String NODE_NAV = "nav";
public static final String NODE_PIXEL_DENSITY = "pixel-density";
public static final String NODE_SCREEN_ORIENTATION = "screen-orientation";
public static final String NODE_AUTOFOCUS = "autofocus";
public static final String NODE_SCREEN_SIZE = "screen-size";
public static final String NODE_DESCRIPTION = "description";
public static final String NODE_BLUETOOTH_PROFILES = "bluetooth-profiles";
public static final String NODE_SCREEN = "screen";
public static final String NODE_SENSORS = "sensors";
public static final String NODE_DIAGONAL_LENGTH = "diagonal-length";
public static final String NODE_SCREEN_TYPE = "screen-type";
public static final String NODE_KEYBOARD_STATE = "keyboard-state";
public static final String NODE_X_DIMENSION = "x-dimension";
public static final String NODE_CPU = "cpu";
public static final String NODE_INTERNAL_STORAGE = "internal-storage";
public static final String NODE_META = "meta";
public static final String NODE_ICONS = "icons";
public static final String NODE_SIXTY_FOUR = "sixty-four";
public static final String NODE_SIXTEEN = "sixteen";
public static final String NODE_FRAME = "frame";
public static final String NODE_PATH = "path";
public static final String NODE_PORTRAIT_X_OFFSET = "portrait-x-offset";
public static final String NODE_PORTRAIT_Y_OFFSET = "portrait-y-offset";
public static final String NODE_LANDSCAPE_X_OFFSET = "landscape-x-offset";
public static final String NODE_LANDSCAPE_Y_OFFSET = "landscape-y-offset";
public static final String NODE_NAME = "name";
public static final String NODE_ID = "id";
public static final String NODE_API_LEVEL = "api-level";
public static final String NODE_MANUFACTURER = "manufacturer";
public static final String NODE_TAG_ID = "tag-id";
public static final String NODE_BOOT_PROPS = "boot-props";
public static final String NODE_BOOT_PROP = "boot-prop";
public static final String NODE_PROP_NAME = "prop-name";
public static final String NODE_PROP_VALUE = "prop-value";
public static final String ATTR_DEFAULT = "default";
public static final String ATTR_UNIT = "unit";
public static final String ATTR_NAME = "name";
/**
* Returns the URI of the SDK Repository schema for the given version number.
* @param version Between 1 and {@link #NS_LATEST_VERSION} included.
*/
public static String getSchemaUri(int version) {
return String.format(NS_DEVICES_URI_BASE + "%d", version); //$NON-NLS-1$
}
/**
* Returns a stream to the requested {@code device} XML Schema.
*
* @param version Between 1 and {@link #NS_LATEST_VERSION}, included.
* @return An {@link InputStream} object for the local XSD file or
* null if there is no schema for the requested version.
*/
public static InputStream getXsdStream(int version) {
assert version >= 1 && version <= NS_LATEST_VERSION;
String rootElement = DeviceSchema.NODE_DEVICES; //$NON-NLS-1$
String filename = String.format("%1$s-%2$d.xsd", rootElement, version); //$NON-NLS-1$
try {
return DeviceSchema.class.getResourceAsStream(filename);
} catch (Exception ignore) {
// Some implementations seem to return null on failure,
// others throw an exception. We want to return null.
}
return null;
}
/**
* Validates the input stream against the corresponding Devices XSD schema
* and then does a sanity check on the content.
*
* @param deviceXml The XML InputStream to validate.
* The XML input stream must supports the mark/reset() methods
* (that is its {@link InputStream#markSupported()} must return true)
* and which mark has already been set to the beginning of the stream.
* @param out The OutputStream for error messages.
* @param parent The parent directory of the input stream.
* @return Whether the given input constitutes a valid devices file.
*/
public static boolean validate(InputStream deviceXml, OutputStream out, File parent) {
PrintWriter writer = new PrintWriter(out);
try {
if (!(deviceXml instanceof NonClosingInputStream)) {
deviceXml = new NonClosingInputStream(deviceXml);
((NonClosingInputStream) deviceXml).setCloseBehavior(CloseBehavior.RESET);
}
int version = getXmlSchemaVersion(deviceXml);
if (version < 1 || version > NS_LATEST_VERSION) {
writer.println(String.format("Devices XSD version %1$d is out of valid range 1..%2$d",
version, NS_LATEST_VERSION));
return false;
}
assert deviceXml.markSupported();
// First check the input against the XSD schema
// Check the input, both against the XSD schema discovered above and also
// by using a custom validation which tests some properties not encoded in the XSD.
Schema schema = DeviceSchema.getSchema(version);
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
factory.setSchema(schema);
DevicesValidationHandler devicesValidator = new DevicesValidationHandler(parent, writer);
SAXParser parser = factory.newSAXParser();
deviceXml.reset();
parser.parse(deviceXml, devicesValidator);
return devicesValidator.isValidDevicesFile();
} catch (SAXException e) {
writer.println(e.getMessage());
return false;
} catch (ParserConfigurationException e) {
writer.println("Error creating SAX parser:");
writer.println(e.getMessage());
return false;
} catch (IOException e) {
writer.println("Error reading file stream:");
writer.println(e.getMessage());
return false;
} finally {
writer.flush();
}
}
/**
* Helper method that returns a validator for a specific version of the XSD.
*
* @param version Between 1 and {@link #NS_LATEST_VERSION}, included.
* @return A {@link Schema} validator or null.
*/
@Nullable
public static Schema getSchema(int version) throws SAXException {
InputStream xsdStream = getXsdStream(version);
if (xsdStream == null) {
return null;
}
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = factory.newSchema(new StreamSource(xsdStream));
return schema;
}
/**
* Manually parses the root element of the XML to extract the schema version
* at the end of the xmlns:sdk="http://schemas.android.com/sdk/devices/$N"
* declaration.
*
* @param xml An XML input stream that supports the mark/reset() methods
* (that is its {@link InputStream#markSupported()} must return true)
* and which mark has already been set to the beginning of the stream.
* @return 1+ for a valid schema version
* or 0 if no schema could be found.
*/
public static int getXmlSchemaVersion(InputStream xml) {
if (xml == null) {
return 0;
}
// Get an XML document
Document doc = null;
try {
assert xml.markSupported();
xml.reset();
if (!(xml instanceof NonClosingInputStream)) {
xml = new NonClosingInputStream(xml);
((NonClosingInputStream) xml).setCloseBehavior(CloseBehavior.RESET);
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setIgnoringComments(false);
factory.setValidating(false);
// Parse the document using a non namespace aware builder
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
// We don't want the default handler which prints errors to stderr.
builder.setErrorHandler(new ErrorHandler() {
@Override
public void warning(SAXParseException e) throws SAXException {
// pass
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
@Override
public void error(SAXParseException e) throws SAXException {
throw e;
}
});
doc = builder.parse(xml);
// Prepare a new document using a namespace aware builder
factory.setNamespaceAware(true);
builder = factory.newDocumentBuilder();
} catch (Exception e) {
// Failed to reset XML stream
// Failed to get builder factor
// Failed to create XML document builder
// Failed to parse XML document
// Failed to read XML document
}
if (doc == null) {
return 0;
}
// Check the root element is an XML with at least the following properties:
// <sdk:sdk-repository
// xmlns:sdk="http://schemas.android.com/sdk/devices/$N">
//
// Note that we don't have namespace support enabled, we just do it manually.
Pattern nsPattern = Pattern.compile(NS_DEVICES_URI_PATTERN);
String prefix = null;
for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNodeType() == Node.ELEMENT_NODE) {
prefix = null;
String name = child.getNodeName();
int pos = name.indexOf(':');
if (pos > 0 && pos < name.length() - 1) {
prefix = name.substring(0, pos);
name = name.substring(pos + 1);
}
if (NODE_DEVICES.equals(name)) {
NamedNodeMap attrs = child.getAttributes();
String xmlns = "xmlns"; //$NON-NLS-1$
if (prefix != null) {
xmlns += ":" + prefix; //$NON-NLS-1$
}
Node attr = attrs.getNamedItem(xmlns);
if (attr != null) {
String uri = attr.getNodeValue();
if (uri != null) {
Matcher m = nsPattern.matcher(uri);
if (m.matches()) {
String version = m.group(1);
try {
return Integer.parseInt(version);
} catch (NumberFormatException e) {
return 0;
}
}
}
}
}
}
}
return 0;
}
/**
* A DefaultHandler that parses only to validate the XML is actually a valid
* devices config, since validation can't be entirely encoded in the devices
* schema.
*/
private static class DevicesValidationHandler extends DefaultHandler {
private boolean mValidDevicesFile = true;
private boolean mDefaultSeen = false;
private String mDeviceName;
private final File mDirectory;
private final PrintWriter mWriter;
private final StringBuilder mStringAccumulator = new StringBuilder();
public DevicesValidationHandler(File directory, PrintWriter writer) {
mDirectory = directory; // Possibly null
mWriter = writer;
}
@Override
public void startElement(String uri, String localName, String name, Attributes attributes)
throws SAXException {
if (NODE_DEVICE.equals(localName)) {
// Reset for a new device
mDefaultSeen = false;
} else if (NODE_STATE.equals(localName)) {
// Check if the state is set to be a default state
String val = attributes.getValue(ATTR_DEFAULT);
if (val != null && ("1".equals(val) || Boolean.parseBoolean(val))) {
/*
* If it is and we already have a default state for this
* device, then the device configuration is invalid.
* Otherwise, set that we've seen a default state for this
* device and continue
*/
if (mDefaultSeen) {
validationError("More than one default state for device " + mDeviceName);
} else {
mDefaultSeen = true;
}
}
}
mStringAccumulator.setLength(0);
}
@Override
public void characters(char[] ch, int start, int length) {
mStringAccumulator.append(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
// If this is the end of a device node, make sure we have at least
// one default state
if (NODE_DEVICE.equals(localName) && !mDefaultSeen) {
validationError("No default state for device " + mDeviceName);
} else if (NODE_NAME.equals(localName)) {
mDeviceName = mStringAccumulator.toString().trim();
} else if (NODE_PATH.equals(localName) || NODE_SIXTY_FOUR.equals(localName)
|| NODE_SIXTEEN.equals(localName)) {
if (mDirectory == null) {
// There is no given parent directory, so this is not a
// valid devices file
validationError("No parent directory given, but relative paths exist.");
return;
}
// This is going to break on any files that end with a space,
// but that should be an incredibly rare corner case.
String relativePath = mStringAccumulator.toString().trim();
File f = new File(mDirectory, relativePath);
if (f == null || !f.isFile()) {
validationError(relativePath + " is not a valid path.");
return;
}
String fileName = f.getName();
int extensionStart = fileName.lastIndexOf('.');
if (extensionStart == -1 || !fileName.substring(extensionStart + 1).equals("png")) {
validationError(relativePath + " is not a valid file type.");
}
}
}
@Override
public void error(SAXParseException e) {
validationError(e.getMessage());
}
@Override
public void fatalError(SAXParseException e) {
validationError(e.getMessage());
}
public boolean isValidDevicesFile() {
return mValidDevicesFile;
}
private void validationError(String reason) {
mWriter.println("Error: " + reason);
mValidDevicesFile = false;
}
}
}