|  | /* | 
|  | * 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 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(); | 
|  | } | 
|  | } | 
|  | } | 
|  | } |