/* * 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="android.content.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 Man 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(); } } } }