476 lines
20 KiB
Java
476 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2011 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.example.android.xmladapters;
|
|
|
|
import org.apache.http.HttpEntity;
|
|
import org.apache.http.HttpResponse;
|
|
import org.apache.http.HttpStatus;
|
|
import org.apache.http.client.methods.HttpGet;
|
|
|
|
import android.content.ContentProvider;
|
|
import android.content.ContentResolver;
|
|
import android.content.ContentValues;
|
|
import android.content.pm.PackageManager.NameNotFoundException;
|
|
import android.content.res.Resources;
|
|
import android.database.Cursor;
|
|
import android.database.MatrixCursor;
|
|
import android.net.Uri;
|
|
import android.net.http.AndroidHttpClient;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.widget.CursorAdapter;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.BitSet;
|
|
import java.util.List;
|
|
import java.util.Stack;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
*
|
|
* A read-only content provider which extracts data out of an XML document.
|
|
*
|
|
* <p>A XPath-like selection pattern is used to select some nodes in the XML document. Each such
|
|
* node will create a row in the {@link Cursor} result.</p>
|
|
*
|
|
* Each row is then populated with columns that are also defined as XPath-like projections. These
|
|
* projections fetch attributes values or text in the matching row node or its children.
|
|
*
|
|
* <p>To add this provider in your application, you should add its declaration to your application
|
|
* manifest:
|
|
* <pre class="prettyprint">
|
|
* <provider android:name="XmlDocumentProvider" android:authorities="xmldocument" />
|
|
* </pre>
|
|
* </p>
|
|
*
|
|
* <h2>Node selection syntax</h2>
|
|
* The node selection syntax is made of the concatenation of an arbitrary number (at least one) of
|
|
* <code>/node_name</code> node selection patterns.
|
|
*
|
|
* <p>The <code>/root/child1/child2</code> pattern will for instance match all nodes named
|
|
* <code>child2</code> which are children of a node named <code>child1</code> which are themselves
|
|
* children of a root node named <code>root</code>.</p>
|
|
*
|
|
* Any <code>/</code> separator in the previous expression can be replaced by a <code>//</code>
|
|
* separator instead, which indicated a <i>descendant</i> instead of a child.
|
|
*
|
|
* <p>The <code>//node1//node2</code> pattern will for instance match all nodes named
|
|
* <code>node2</code> which are descendant of a node named <code>node1</code> located anywhere in
|
|
* the document hierarchy.</p>
|
|
*
|
|
* Node names can contain namespaces in the form <code>namespace:node</code>.
|
|
*
|
|
* <h2>Projection syntax</h2>
|
|
* For every selected node, the projection will then extract actual data from this node and its
|
|
* descendant.
|
|
*
|
|
* <p>Use a syntax similar to the selection syntax described above to select the text associated
|
|
* with a child of the selected node. The implicit root of this projection pattern is the selected
|
|
* node. <code>/</code> will hence refer to the text of the selected node, while
|
|
* <code>/child1</code> will fetch the text of its child named <code>child1</code> and
|
|
* <code>//child1</code> will match any <i>descendant</i> named <code>child1</code>. If several
|
|
* nodes match the projection pattern, their texts are appended as a result.</p>
|
|
*
|
|
* A projection can also fetch any node attribute by appending a <code>@attribute_name</code>
|
|
* pattern to the previously described syntax. <code>//child1@price</code> will for instance match
|
|
* the attribute <code>price</code> of any <code>child1</code> descendant.
|
|
*
|
|
* <p>If a projection does not match any node/attribute, its associated value will be an empty
|
|
* string.</p>
|
|
*
|
|
* <h2>Example</h2>
|
|
* Using the following XML document:
|
|
* <pre class="prettyprint">
|
|
* <library>
|
|
* <book id="EH94">
|
|
* <title>The Old Person and the Sea</title>
|
|
* <author>Ernest Hemingway</author>
|
|
* </book>
|
|
* <book id="XX10">
|
|
* <title>The Arabian Nights: Tales of 1,001 Nights</title>
|
|
* </book>
|
|
* <no-id>
|
|
* <book>
|
|
* <title>Animal Farm</title>
|
|
* <author>George Orwell</author>
|
|
* </book>
|
|
* </no-id>
|
|
* </library>
|
|
* </pre>
|
|
* A selection pattern of <code>/library//book</code> will match the three book entries (while
|
|
* <code>/library/book</code> will only match the first two ones).
|
|
*
|
|
* <p>Defining the projections as <code>/title</code>, <code>/author</code> and <code>@id</code>
|
|
* will retrieve the associated data. Note that the author of the second book as well as the id of
|
|
* the third are empty strings.
|
|
*/
|
|
public class XmlDocumentProvider extends ContentProvider {
|
|
/*
|
|
* Ideas for improvement:
|
|
* - Expand XPath-like syntax to allow for [nb] child number selector
|
|
* - Address the starting . bug in AbstractCursor which prevents a true XPath syntax.
|
|
* - Provide an alternative to concatenation when several node match (list-like).
|
|
* - Support namespaces in attribute names.
|
|
* - Incremental Cursor creation, pagination
|
|
*/
|
|
private static final String LOG_TAG = "XmlDocumentProvider";
|
|
private AndroidHttpClient mHttpClient;
|
|
|
|
@Override
|
|
public boolean onCreate() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Query data from the XML document referenced in the URI.
|
|
*
|
|
* <p>The XML document can be a local resource or a file that will be downloaded from the
|
|
* Internet. In the latter case, your application needs to request the INTERNET permission in
|
|
* its manifest.</p>
|
|
*
|
|
* The URI will be of the form <code>content://xmldocument/?resource=R.xml.myFile</code> for a
|
|
* local resource. <code>xmldocument</code> should match the authority declared for this
|
|
* provider in your manifest. Internet documents are referenced using
|
|
* <code>content://xmldocument/?url=</code> followed by an encoded version of the URL of your
|
|
* document (see {@link Uri#encode(String)}).
|
|
*
|
|
* <p>The number of columns of the resulting Cursor is equal to the size of the projection
|
|
* array plus one, named <code>_id</code> which will contain a unique row id (allowing the
|
|
* Cursor to be used with a {@link CursorAdapter}). The other columns' names are the projection
|
|
* patterns.</p>
|
|
*
|
|
* @param uri The URI of your local resource or Internet document.
|
|
* @param projection A set of patterns that will be used to extract data from each selected
|
|
* node. See class documentation for pattern syntax.
|
|
* @param selection A selection pattern which will select the nodes that will create the
|
|
* Cursor's rows. See class documentation for pattern syntax.
|
|
* @param selectionArgs This parameter is ignored.
|
|
* @param sortOrder The row order in the resulting cursor is determined from the node order in
|
|
* the XML document. This parameter is ignored.
|
|
* @return A Cursor or null in case of error.
|
|
*/
|
|
@Override
|
|
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
|
String sortOrder) {
|
|
|
|
XmlPullParser parser = null;
|
|
mHttpClient = null;
|
|
|
|
final String url = uri.getQueryParameter("url");
|
|
if (url != null) {
|
|
parser = getUriXmlPullParser(url);
|
|
} else {
|
|
final String resource = uri.getQueryParameter("resource");
|
|
if (resource != null) {
|
|
Uri resourceUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
|
|
getContext().getPackageName() + "/" + resource);
|
|
parser = getResourceXmlPullParser(resourceUri);
|
|
}
|
|
}
|
|
|
|
if (parser != null) {
|
|
XMLCursor xmlCursor = new XMLCursor(selection, projection);
|
|
try {
|
|
xmlCursor.parseWith(parser);
|
|
return xmlCursor;
|
|
} catch (IOException e) {
|
|
Log.w(LOG_TAG, "I/O error while parsing XML " + uri, e);
|
|
} catch (XmlPullParserException e) {
|
|
Log.w(LOG_TAG, "Error while parsing XML " + uri, e);
|
|
} finally {
|
|
if (mHttpClient != null) {
|
|
mHttpClient.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Creates an XmlPullParser for the provided URL. Can be overloaded to provide your own parser.
|
|
* @param url The URL of the XML document that is to be parsed.
|
|
* @return An XmlPullParser on this document.
|
|
*/
|
|
protected XmlPullParser getUriXmlPullParser(String url) {
|
|
XmlPullParser parser = null;
|
|
try {
|
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
|
factory.setNamespaceAware(true);
|
|
parser = factory.newPullParser();
|
|
} catch (XmlPullParserException e) {
|
|
Log.e(LOG_TAG, "Unable to create XmlPullParser", e);
|
|
return null;
|
|
}
|
|
|
|
InputStream inputStream = null;
|
|
try {
|
|
final HttpGet get = new HttpGet(url);
|
|
mHttpClient = AndroidHttpClient.newInstance("Android");
|
|
HttpResponse response = mHttpClient.execute(get);
|
|
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
|
final HttpEntity entity = response.getEntity();
|
|
if (entity != null) {
|
|
inputStream = entity.getContent();
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
Log.w(LOG_TAG, "Error while retrieving XML file " + url, e);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
parser.setInput(inputStream, null);
|
|
} catch (XmlPullParserException e) {
|
|
Log.w(LOG_TAG, "Error while reading XML file from " + url, e);
|
|
return null;
|
|
}
|
|
|
|
return parser;
|
|
}
|
|
|
|
/**
|
|
* Creates an XmlPullParser for the provided local resource. Can be overloaded to provide your
|
|
* own parser.
|
|
* @param resourceUri A fully qualified resource name referencing a local XML resource.
|
|
* @return An XmlPullParser on this resource.
|
|
*/
|
|
protected XmlPullParser getResourceXmlPullParser(Uri resourceUri) {
|
|
//OpenResourceIdResult resourceId;
|
|
try {
|
|
String authority = resourceUri.getAuthority();
|
|
Resources r;
|
|
if (TextUtils.isEmpty(authority)) {
|
|
throw new FileNotFoundException("No authority: " + resourceUri);
|
|
} else {
|
|
try {
|
|
r = getContext().getPackageManager().getResourcesForApplication(authority);
|
|
} catch (NameNotFoundException ex) {
|
|
throw new FileNotFoundException("No package found for authority: " + resourceUri);
|
|
}
|
|
}
|
|
List<String> path = resourceUri.getPathSegments();
|
|
if (path == null) {
|
|
throw new FileNotFoundException("No path: " + resourceUri);
|
|
}
|
|
int len = path.size();
|
|
int id;
|
|
if (len == 1) {
|
|
try {
|
|
id = Integer.parseInt(path.get(0));
|
|
} catch (NumberFormatException e) {
|
|
throw new FileNotFoundException("Single path segment is not a resource ID: " + resourceUri);
|
|
}
|
|
} else if (len == 2) {
|
|
id = r.getIdentifier(path.get(1), path.get(0), authority);
|
|
} else {
|
|
throw new FileNotFoundException("More than two path segments: " + resourceUri);
|
|
}
|
|
if (id == 0) {
|
|
throw new FileNotFoundException("No resource found for: " + resourceUri);
|
|
}
|
|
|
|
return r.getXml(id);
|
|
} catch (FileNotFoundException e) {
|
|
Log.w(LOG_TAG, "XML resource not found: " + resourceUri.toString(), e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns "vnd.android.cursor.dir/xmldoc".
|
|
*/
|
|
@Override
|
|
public String getType(Uri uri) {
|
|
return "vnd.android.cursor.dir/xmldoc";
|
|
}
|
|
|
|
/**
|
|
* This ContentProvider is read-only. This method throws an UnsupportedOperationException.
|
|
**/
|
|
@Override
|
|
public Uri insert(Uri uri, ContentValues values) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
/**
|
|
* This ContentProvider is read-only. This method throws an UnsupportedOperationException.
|
|
**/
|
|
@Override
|
|
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
/**
|
|
* This ContentProvider is read-only. This method throws an UnsupportedOperationException.
|
|
**/
|
|
@Override
|
|
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
private static class XMLCursor extends MatrixCursor {
|
|
private final Pattern mSelectionPattern;
|
|
private Pattern[] mProjectionPatterns;
|
|
private String[] mAttributeNames;
|
|
private String[] mCurrentValues;
|
|
private BitSet[] mActiveTextDepthMask;
|
|
private final int mNumberOfProjections;
|
|
|
|
public XMLCursor(String selection, String[] projections) {
|
|
super(projections);
|
|
// The first column in projections is used for the _ID
|
|
mNumberOfProjections = projections.length - 1;
|
|
mSelectionPattern = createPattern(selection);
|
|
createProjectionPattern(projections);
|
|
}
|
|
|
|
private Pattern createPattern(String input) {
|
|
String pattern = input.replaceAll("//", "/(.*/|)").replaceAll("^/", "^/") + "$";
|
|
return Pattern.compile(pattern);
|
|
}
|
|
|
|
private void createProjectionPattern(String[] projections) {
|
|
mProjectionPatterns = new Pattern[mNumberOfProjections];
|
|
mAttributeNames = new String[mNumberOfProjections];
|
|
mActiveTextDepthMask = new BitSet[mNumberOfProjections];
|
|
// Add a column to store _ID
|
|
mCurrentValues = new String[mNumberOfProjections + 1];
|
|
|
|
for (int i=0; i<mNumberOfProjections; i++) {
|
|
mActiveTextDepthMask[i] = new BitSet();
|
|
String projection = projections[i + 1]; // +1 to skip the _ID column
|
|
int atIndex = projection.lastIndexOf('@', projection.length());
|
|
if (atIndex >= 0) {
|
|
mAttributeNames[i] = projection.substring(atIndex+1);
|
|
projection = projection.substring(0, atIndex);
|
|
} else {
|
|
mAttributeNames[i] = null;
|
|
}
|
|
|
|
// Conforms to XPath standard: reference to local context starts with a .
|
|
if (projection.charAt(0) == '.') {
|
|
projection = projection.substring(1);
|
|
}
|
|
mProjectionPatterns[i] = createPattern(projection);
|
|
}
|
|
}
|
|
|
|
public void parseWith(XmlPullParser parser) throws IOException, XmlPullParserException {
|
|
StringBuilder path = new StringBuilder();
|
|
Stack<Integer> pathLengthStack = new Stack<Integer>();
|
|
|
|
// There are two parsing mode: in root mode, rootPath is updated and nodes matching
|
|
// selectionPattern are searched for and currentNodeDepth is negative.
|
|
// When a node matching selectionPattern is found, currentNodeDepth is set to 0 and
|
|
// updated as children are parsed and projectionPatterns are searched in nodePath.
|
|
int currentNodeDepth = -1;
|
|
|
|
// Index where local selected node path starts from in path
|
|
int currentNodePathStartIndex = 0;
|
|
|
|
int eventType = parser.getEventType();
|
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
|
|
|
if (eventType == XmlPullParser.START_TAG) {
|
|
// Update path
|
|
pathLengthStack.push(path.length());
|
|
path.append('/');
|
|
String prefix = null;
|
|
try {
|
|
// getPrefix is not supported by local Xml resource parser
|
|
prefix = parser.getPrefix();
|
|
} catch (RuntimeException e) {
|
|
prefix = null;
|
|
}
|
|
if (prefix != null) {
|
|
path.append(prefix);
|
|
path.append(':');
|
|
}
|
|
path.append(parser.getName());
|
|
|
|
if (currentNodeDepth >= 0) {
|
|
currentNodeDepth++;
|
|
} else {
|
|
// A node matching selection is found: initialize child parsing mode
|
|
if (mSelectionPattern.matcher(path.toString()).matches()) {
|
|
currentNodeDepth = 0;
|
|
currentNodePathStartIndex = path.length();
|
|
mCurrentValues[0] = Integer.toString(getCount()); // _ID
|
|
for (int i = 0; i < mNumberOfProjections; i++) {
|
|
// Reset values to default (empty string)
|
|
mCurrentValues[i + 1] = "";
|
|
mActiveTextDepthMask[i].clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
// This test has to be separated from the previous one as currentNodeDepth can
|
|
// be modified above (when a node matching selection is found).
|
|
if (currentNodeDepth >= 0) {
|
|
final String localNodePath = path.substring(currentNodePathStartIndex);
|
|
for (int i = 0; i < mNumberOfProjections; i++) {
|
|
if (mProjectionPatterns[i].matcher(localNodePath).matches()) {
|
|
String attribute = mAttributeNames[i];
|
|
if (attribute != null) {
|
|
mCurrentValues[i + 1] =
|
|
parser.getAttributeValue(null, attribute);
|
|
} else {
|
|
mActiveTextDepthMask[i].set(currentNodeDepth, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} else if (eventType == XmlPullParser.END_TAG) {
|
|
// Pop last node from path
|
|
final int length = pathLengthStack.pop();
|
|
path.setLength(length);
|
|
|
|
if (currentNodeDepth >= 0) {
|
|
if (currentNodeDepth == 0) {
|
|
// Leaving a selection matching node: add a new row with results
|
|
addRow(mCurrentValues);
|
|
} else {
|
|
for (int i = 0; i < mNumberOfProjections; i++) {
|
|
mActiveTextDepthMask[i].set(currentNodeDepth, false);
|
|
}
|
|
}
|
|
currentNodeDepth--;
|
|
}
|
|
|
|
} else if ((eventType == XmlPullParser.TEXT) && (!parser.isWhitespace())) {
|
|
for (int i = 0; i < mNumberOfProjections; i++) {
|
|
if ((currentNodeDepth >= 0) &&
|
|
(mActiveTextDepthMask[i].get(currentNodeDepth))) {
|
|
mCurrentValues[i + 1] += parser.getText();
|
|
}
|
|
}
|
|
}
|
|
|
|
eventType = parser.next();
|
|
}
|
|
}
|
|
}
|
|
}
|