diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java b/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java index 9d4794c08..2fbfb3485 100644 --- a/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java @@ -16,9 +16,6 @@ package com.example.android.xmladapters; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import android.app.Activity; import android.content.Context; import android.content.res.Resources; @@ -37,6 +34,9 @@ import android.widget.ImageView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -257,7 +257,6 @@ import java.util.HashMap; * attr ref android.R.styleable#CursorAdapter_TransformItem_withClass * attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression */ -@SuppressWarnings({"JavadocReference"}) public class Adapters { private static final String ADAPTER_CURSOR = "cursor-adapter"; @@ -898,10 +897,13 @@ public class Adapters { * of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders. */ private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter { + private Context mContext; private String mUri; private final String mSelection; private final String[] mSelectionArgs; private final String mSortOrder; + private final int[] mTo; + private final String[] mFrom; private final String[] mColumns; private final CursorBinder[] mBinders; private AsyncTask mLoadTask; @@ -913,6 +915,8 @@ public class Adapters { super(context, layout, null, from, to); mContext = context; mUri = uri; + mFrom = from; + mTo = to; mSelection = selection; mSelectionArgs = selectionArgs; mSortOrder = sortOrder; @@ -935,14 +939,14 @@ public class Adapters { @Override public void bindView(View view, Context context, Cursor cursor) { final int count = mTo.length; - final int[] from = mFrom; final int[] to = mTo; final CursorBinder[] binders = mBinders; for (int i = 0; i < count; i++) { final View v = view.findViewById(to[i]); if (v != null) { - binders[i].bind(v, cursor, from[i]); + // Not optimal, the column index could be cached + binders[i].bind(v, cursor, cursor.getColumnIndex(mFrom[i])); } } } diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java b/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java index c84f9d528..eb91fcbb9 100644 --- a/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java @@ -12,7 +12,9 @@ * 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; + */ + +package com.example.android.xmladapters; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -58,14 +60,16 @@ public class ImageDownloader { // Hard cache, with a fixed maximum capacity and a life duration private final HashMap sHardBitmapCache = new LinkedHashMap(HARD_CACHE_CAPACITY / 2, 0.75f, true) { + private static final long serialVersionUID = -7190622541619388252L; @Override protected boolean removeEldestEntry(LinkedHashMap.Entry eldest) { if (size() > HARD_CACHE_CAPACITY) { // Entries push-out of hard reference cache are transferred to soft reference cache sSoftBitmapCache.put(eldest.getKey(), new SoftReference(eldest.getValue())); return true; - } else + } else { return false; + } } }; diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java b/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java index 16df24654..defdc19c4 100644 --- a/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java @@ -17,7 +17,6 @@ package com.example.android.xmladapters; import android.app.ListActivity; -import android.content.XmlDocumentProvider; import android.net.Uri; import android.os.Bundle; import android.widget.AdapterView.OnItemClickListener; diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/XmlDocumentProvider.java b/samples/XmlAdapters/src/com/example/android/xmladapters/XmlDocumentProvider.java new file mode 100644 index 000000000..d0ab0cfe7 --- /dev/null +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/XmlDocumentProvider.java @@ -0,0 +1,475 @@ +/* + * 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. + * + *

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.

+ * + * 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. + * + *

To add this provider in your application, you should add its declaration to your application + * manifest: + *

+ * <provider android:name="android.content.XmlDocumentProvider" android:authorities="xmldocument" />
+ * 
+ *

+ * + *

Node selection syntax

+ * The node selection syntax is made of the concatenation of an arbitrary number (at least one) of + * /node_name node selection patterns. + * + *

The /root/child1/child2 pattern will for instance match all nodes named + * child2 which are children of a node named child1 which are themselves + * children of a root node named root.

+ * + * Any / separator in the previous expression can be replaced by a // + * separator instead, which indicated a descendant instead of a child. + * + *

The //node1//node2 pattern will for instance match all nodes named + * node2 which are descendant of a node named node1 located anywhere in + * the document hierarchy.

+ * + * Node names can contain namespaces in the form namespace:node. + * + *

Projection syntax

+ * For every selected node, the projection will then extract actual data from this node and its + * descendant. + * + *

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. / will hence refer to the text of the selected node, while + * /child1 will fetch the text of its child named child1 and + * //child1 will match any descendant named child1. If several + * nodes match the projection pattern, their texts are appended as a result.

+ * + * A projection can also fetch any node attribute by appending a @attribute_name + * pattern to the previously described syntax. //child1@price will for instance match + * the attribute price of any child1 descendant. + * + *

If a projection does not match any node/attribute, its associated value will be an empty + * string.

+ * + *

Example

+ * Using the following XML document: + *
+ * <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>
+ * 
+ * A selection pattern of /library//book will match the three book entries (while + * /library/book will only match the first two ones). + * + *

Defining the projections as /title, /author and @id + * 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. + * + *

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.

+ * + * The URI will be of the form content://xmldocument/?resource=R.xml.myFile for a + * local resource. xmldocument should match the authority declared for this + * provider in your manifest. Internet documents are referenced using + * content://xmldocument/?url= followed by an encoded version of the URL of your + * document (see {@link Uri#encode(String)}). + * + *

The number of columns of the resulting Cursor is equal to the size of the projection + * array plus one, named _id 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.

+ * + * @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 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= 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 pathLengthStack = new Stack(); + + // 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(); + } + } + } +}