Home | History | Annotate | Download | only in xmladapters
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.xmladapters;
     18 
     19 import org.apache.http.HttpEntity;
     20 import org.apache.http.HttpResponse;
     21 import org.apache.http.HttpStatus;
     22 import org.apache.http.client.methods.HttpGet;
     23 
     24 import android.content.ContentProvider;
     25 import android.content.ContentResolver;
     26 import android.content.ContentValues;
     27 import android.content.pm.PackageManager.NameNotFoundException;
     28 import android.content.res.Resources;
     29 import android.database.Cursor;
     30 import android.database.MatrixCursor;
     31 import android.net.Uri;
     32 import android.net.http.AndroidHttpClient;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import android.widget.CursorAdapter;
     36 
     37 import org.xmlpull.v1.XmlPullParser;
     38 import org.xmlpull.v1.XmlPullParserException;
     39 import org.xmlpull.v1.XmlPullParserFactory;
     40 
     41 import java.io.FileNotFoundException;
     42 import java.io.IOException;
     43 import java.io.InputStream;
     44 import java.util.BitSet;
     45 import java.util.List;
     46 import java.util.Stack;
     47 import java.util.regex.Pattern;
     48 
     49 /**
     50  *
     51  * A read-only content provider which extracts data out of an XML document.
     52  *
     53  * <p>A XPath-like selection pattern is used to select some nodes in the XML document. Each such
     54  * node will create a row in the {@link Cursor} result.</p>
     55  *
     56  * Each row is then populated with columns that are also defined as XPath-like projections. These
     57  * projections fetch attributes values or text in the matching row node or its children.
     58  *
     59  * <p>To add this provider in your application, you should add its declaration to your application
     60  * manifest:
     61  * <pre class="prettyprint">
     62  * &lt;provider android:name="XmlDocumentProvider" android:authorities="xmldocument" /&gt;
     63  * </pre>
     64  * </p>
     65  *
     66  * <h2>Node selection syntax</h2>
     67  * The node selection syntax is made of the concatenation of an arbitrary number (at least one) of
     68  * <code>/node_name</code> node selection patterns.
     69  *
     70  * <p>The <code>/root/child1/child2</code> pattern will for instance match all nodes named
     71  * <code>child2</code> which are children of a node named <code>child1</code> which are themselves
     72  * children of a root node named <code>root</code>.</p>
     73  *
     74  * Any <code>/</code> separator in the previous expression can be replaced by a <code>//</code>
     75  * separator instead, which indicated a <i>descendant</i> instead of a child.
     76  *
     77  * <p>The <code>//node1//node2</code> pattern will for instance match all nodes named
     78  * <code>node2</code> which are descendant of a node named <code>node1</code> located anywhere in
     79  * the document hierarchy.</p>
     80  *
     81  * Node names can contain namespaces in the form <code>namespace:node</code>.
     82  *
     83  * <h2>Projection syntax</h2>
     84  * For every selected node, the projection will then extract actual data from this node and its
     85  * descendant.
     86  *
     87  * <p>Use a syntax similar to the selection syntax described above to select the text associated
     88  * with a child of the selected node. The implicit root of this projection pattern is the selected
     89  * node. <code>/</code> will hence refer to the text of the selected node, while
     90  * <code>/child1</code> will fetch the text of its child named <code>child1</code> and
     91  * <code>//child1</code> will match any <i>descendant</i> named <code>child1</code>. If several
     92  * nodes match the projection pattern, their texts are appended as a result.</p>
     93  *
     94  * A projection can also fetch any node attribute by appending a <code>@attribute_name</code>
     95  * pattern to the previously described syntax. <code>//child1@price</code> will for instance match
     96  * the attribute <code>price</code> of any <code>child1</code> descendant.
     97  *
     98  * <p>If a projection does not match any node/attribute, its associated value will be an empty
     99  * string.</p>
    100  *
    101  * <h2>Example</h2>
    102  * Using the following XML document:
    103  * <pre class="prettyprint">
    104  * &lt;library&gt;
    105  *   &lt;book id="EH94"&gt;
    106  *     &lt;title&gt;The Old Man and the Sea&lt;/title&gt;
    107  *     &lt;author&gt;Ernest Hemingway&lt;/author&gt;
    108  *   &lt;/book&gt;
    109  *   &lt;book id="XX10"&gt;
    110  *     &lt;title&gt;The Arabian Nights: Tales of 1,001 Nights&lt;/title&gt;
    111  *   &lt;/book&gt;
    112  *   &lt;no-id&gt;
    113  *     &lt;book&gt;
    114  *       &lt;title&gt;Animal Farm&lt;/title&gt;
    115  *       &lt;author&gt;George Orwell&lt;/author&gt;
    116  *     &lt;/book&gt;
    117  *   &lt;/no-id&gt;
    118  * &lt;/library&gt;
    119  * </pre>
    120  * A selection pattern of <code>/library//book</code> will match the three book entries (while
    121  * <code>/library/book</code> will only match the first two ones).
    122  *
    123  * <p>Defining the projections as <code>/title</code>, <code>/author</code> and <code>@id</code>
    124  * will retrieve the associated data. Note that the author of the second book as well as the id of
    125  * the third are empty strings.
    126  */
    127 public class XmlDocumentProvider extends ContentProvider {
    128     /*
    129      * Ideas for improvement:
    130      * - Expand XPath-like syntax to allow for [nb] child number selector
    131      * - Address the starting . bug in AbstractCursor which prevents a true XPath syntax.
    132      * - Provide an alternative to concatenation when several node match (list-like).
    133      * - Support namespaces in attribute names.
    134      * - Incremental Cursor creation, pagination
    135      */
    136     private static final String LOG_TAG = "XmlDocumentProvider";
    137     private AndroidHttpClient mHttpClient;
    138 
    139     @Override
    140     public boolean onCreate() {
    141         return true;
    142     }
    143 
    144     /**
    145      * Query data from the XML document referenced in the URI.
    146      *
    147      * <p>The XML document can be a local resource or a file that will be downloaded from the
    148      * Internet. In the latter case, your application needs to request the INTERNET permission in
    149      * its manifest.</p>
    150      *
    151      * The URI will be of the form <code>content://xmldocument/?resource=R.xml.myFile</code> for a
    152      * local resource. <code>xmldocument</code> should match the authority declared for this
    153      * provider in your manifest. Internet documents are referenced using
    154      * <code>content://xmldocument/?url=</code> followed by an encoded version of the URL of your
    155      * document (see {@link Uri#encode(String)}).
    156      *
    157      * <p>The number of columns of the resulting Cursor is equal to the size of the projection
    158      * array plus one, named <code>_id</code> which will contain a unique row id (allowing the
    159      * Cursor to be used with a {@link CursorAdapter}). The other columns' names are the projection
    160      * patterns.</p>
    161      *
    162      * @param uri The URI of your local resource or Internet document.
    163      * @param projection A set of patterns that will be used to extract data from each selected
    164      * node. See class documentation for pattern syntax.
    165      * @param selection A selection pattern which will select the nodes that will create the
    166      * Cursor's rows. See class documentation for pattern syntax.
    167      * @param selectionArgs This parameter is ignored.
    168      * @param sortOrder The row order in the resulting cursor is determined from the node order in
    169      * the XML document. This parameter is ignored.
    170      * @return A Cursor or null in case of error.
    171      */
    172     @Override
    173     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    174             String sortOrder) {
    175 
    176         XmlPullParser parser = null;
    177         mHttpClient = null;
    178 
    179         final String url = uri.getQueryParameter("url");
    180         if (url != null) {
    181             parser = getUriXmlPullParser(url);
    182         } else {
    183             final String resource = uri.getQueryParameter("resource");
    184             if (resource != null) {
    185                 Uri resourceUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
    186                         getContext().getPackageName() + "/" + resource);
    187                 parser = getResourceXmlPullParser(resourceUri);
    188             }
    189         }
    190 
    191         if (parser != null) {
    192             XMLCursor xmlCursor = new XMLCursor(selection, projection);
    193             try {
    194                 xmlCursor.parseWith(parser);
    195                 return xmlCursor;
    196             } catch (IOException e) {
    197                 Log.w(LOG_TAG, "I/O error while parsing XML " + uri, e);
    198             } catch (XmlPullParserException e) {
    199                 Log.w(LOG_TAG, "Error while parsing XML " + uri, e);
    200             } finally {
    201                 if (mHttpClient != null) {
    202                     mHttpClient.close();
    203                 }
    204             }
    205         }
    206 
    207         return null;
    208     }
    209 
    210     /**
    211      * Creates an XmlPullParser for the provided URL. Can be overloaded to provide your own parser.
    212      * @param url The URL of the XML document that is to be parsed.
    213      * @return An XmlPullParser on this document.
    214      */
    215     protected XmlPullParser getUriXmlPullParser(String url) {
    216         XmlPullParser parser = null;
    217         try {
    218             XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
    219             factory.setNamespaceAware(true);
    220             parser = factory.newPullParser();
    221         } catch (XmlPullParserException e) {
    222             Log.e(LOG_TAG, "Unable to create XmlPullParser", e);
    223             return null;
    224         }
    225 
    226         InputStream inputStream = null;
    227         try {
    228             final HttpGet get = new HttpGet(url);
    229             mHttpClient = AndroidHttpClient.newInstance("Android");
    230             HttpResponse response = mHttpClient.execute(get);
    231             if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
    232                 final HttpEntity entity = response.getEntity();
    233                 if (entity != null) {
    234                     inputStream = entity.getContent();
    235                 }
    236             }
    237         } catch (IOException e) {
    238             Log.w(LOG_TAG, "Error while retrieving XML file " + url, e);
    239             return null;
    240         }
    241 
    242         try {
    243             parser.setInput(inputStream, null);
    244         } catch (XmlPullParserException e) {
    245             Log.w(LOG_TAG, "Error while reading XML file from " + url, e);
    246             return null;
    247         }
    248 
    249         return parser;
    250     }
    251 
    252     /**
    253      * Creates an XmlPullParser for the provided local resource. Can be overloaded to provide your
    254      * own parser.
    255      * @param resourceUri A fully qualified resource name referencing a local XML resource.
    256      * @return An XmlPullParser on this resource.
    257      */
    258     protected XmlPullParser getResourceXmlPullParser(Uri resourceUri) {
    259         //OpenResourceIdResult resourceId;
    260         try {
    261             String authority = resourceUri.getAuthority();
    262             Resources r;
    263             if (TextUtils.isEmpty(authority)) {
    264                 throw new FileNotFoundException("No authority: " + resourceUri);
    265             } else {
    266                 try {
    267                     r = getContext().getPackageManager().getResourcesForApplication(authority);
    268                 } catch (NameNotFoundException ex) {
    269                     throw new FileNotFoundException("No package found for authority: " + resourceUri);
    270                 }
    271             }
    272             List<String> path = resourceUri.getPathSegments();
    273             if (path == null) {
    274                 throw new FileNotFoundException("No path: " + resourceUri);
    275             }
    276             int len = path.size();
    277             int id;
    278             if (len == 1) {
    279                 try {
    280                     id = Integer.parseInt(path.get(0));
    281                 } catch (NumberFormatException e) {
    282                     throw new FileNotFoundException("Single path segment is not a resource ID: " + resourceUri);
    283                 }
    284             } else if (len == 2) {
    285                 id = r.getIdentifier(path.get(1), path.get(0), authority);
    286             } else {
    287                 throw new FileNotFoundException("More than two path segments: " + resourceUri);
    288             }
    289             if (id == 0) {
    290                 throw new FileNotFoundException("No resource found for: " + resourceUri);
    291             }
    292 
    293             return r.getXml(id);
    294         } catch (FileNotFoundException e) {
    295             Log.w(LOG_TAG, "XML resource not found: " + resourceUri.toString(), e);
    296             return null;
    297         }
    298     }
    299 
    300     /**
    301      * Returns "vnd.android.cursor.dir/xmldoc".
    302      */
    303     @Override
    304     public String getType(Uri uri) {
    305         return "vnd.android.cursor.dir/xmldoc";
    306     }
    307 
    308     /**
    309      * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
    310      **/
    311     @Override
    312     public Uri insert(Uri uri, ContentValues values) {
    313         throw new UnsupportedOperationException();
    314     }
    315 
    316     /**
    317      * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
    318      **/
    319     @Override
    320     public int delete(Uri uri, String selection, String[] selectionArgs) {
    321         throw new UnsupportedOperationException();
    322     }
    323 
    324     /**
    325      * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
    326      **/
    327     @Override
    328     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    329         throw new UnsupportedOperationException();
    330     }
    331 
    332     private static class XMLCursor extends MatrixCursor {
    333         private final Pattern mSelectionPattern;
    334         private Pattern[] mProjectionPatterns;
    335         private String[] mAttributeNames;
    336         private String[] mCurrentValues;
    337         private BitSet[] mActiveTextDepthMask;
    338         private final int mNumberOfProjections;
    339 
    340         public XMLCursor(String selection, String[] projections) {
    341             super(projections);
    342             // The first column in projections is used for the _ID
    343             mNumberOfProjections = projections.length - 1;
    344             mSelectionPattern = createPattern(selection);
    345             createProjectionPattern(projections);
    346         }
    347 
    348         private Pattern createPattern(String input) {
    349             String pattern = input.replaceAll("//", "/(.*/|)").replaceAll("^/", "^/") + "$";
    350             return Pattern.compile(pattern);
    351         }
    352 
    353         private void createProjectionPattern(String[] projections) {
    354             mProjectionPatterns = new Pattern[mNumberOfProjections];
    355             mAttributeNames = new String[mNumberOfProjections];
    356             mActiveTextDepthMask = new BitSet[mNumberOfProjections];
    357             // Add a column to store _ID
    358             mCurrentValues = new String[mNumberOfProjections + 1];
    359 
    360             for (int i=0; i<mNumberOfProjections; i++) {
    361                 mActiveTextDepthMask[i] = new BitSet();
    362                 String projection = projections[i + 1]; // +1 to skip the _ID column
    363                 int atIndex = projection.lastIndexOf('@', projection.length());
    364                 if (atIndex >= 0) {
    365                     mAttributeNames[i] = projection.substring(atIndex+1);
    366                     projection = projection.substring(0, atIndex);
    367                 } else {
    368                     mAttributeNames[i] = null;
    369                 }
    370 
    371                 // Conforms to XPath standard: reference to local context starts with a .
    372                 if (projection.charAt(0) == '.') {
    373                     projection = projection.substring(1);
    374                 }
    375                 mProjectionPatterns[i] = createPattern(projection);
    376             }
    377         }
    378 
    379         public void parseWith(XmlPullParser parser) throws IOException, XmlPullParserException {
    380             StringBuilder path = new StringBuilder();
    381             Stack<Integer> pathLengthStack = new Stack<Integer>();
    382 
    383             // There are two parsing mode: in root mode, rootPath is updated and nodes matching
    384             // selectionPattern are searched for and currentNodeDepth is negative.
    385             // When a node matching selectionPattern is found, currentNodeDepth is set to 0 and
    386             // updated as children are parsed and projectionPatterns are searched in nodePath.
    387             int currentNodeDepth = -1;
    388 
    389             // Index where local selected node path starts from in path
    390             int currentNodePathStartIndex = 0;
    391 
    392             int eventType = parser.getEventType();
    393             while (eventType != XmlPullParser.END_DOCUMENT) {
    394 
    395                 if (eventType == XmlPullParser.START_TAG) {
    396                     // Update path
    397                     pathLengthStack.push(path.length());
    398                     path.append('/');
    399                     String prefix = null;
    400                     try {
    401                         // getPrefix is not supported by local Xml resource parser
    402                         prefix = parser.getPrefix();
    403                     } catch (RuntimeException e) {
    404                         prefix = null;
    405                     }
    406                     if (prefix != null) {
    407                         path.append(prefix);
    408                         path.append(':');
    409                     }
    410                     path.append(parser.getName());
    411 
    412                     if (currentNodeDepth >= 0) {
    413                         currentNodeDepth++;
    414                     } else {
    415                         // A node matching selection is found: initialize child parsing mode
    416                         if (mSelectionPattern.matcher(path.toString()).matches()) {
    417                             currentNodeDepth = 0;
    418                             currentNodePathStartIndex = path.length();
    419                             mCurrentValues[0] = Integer.toString(getCount()); // _ID
    420                             for (int i = 0; i < mNumberOfProjections; i++) {
    421                                 // Reset values to default (empty string)
    422                                 mCurrentValues[i + 1] = "";
    423                                 mActiveTextDepthMask[i].clear();
    424                             }
    425                         }
    426                     }
    427 
    428                     // This test has to be separated from the previous one as currentNodeDepth can
    429                     // be modified above (when a node matching selection is found).
    430                     if (currentNodeDepth >= 0) {
    431                         final String localNodePath = path.substring(currentNodePathStartIndex);
    432                         for (int i = 0; i < mNumberOfProjections; i++) {
    433                             if (mProjectionPatterns[i].matcher(localNodePath).matches()) {
    434                                 String attribute = mAttributeNames[i];
    435                                 if (attribute != null) {
    436                                     mCurrentValues[i + 1] =
    437                                         parser.getAttributeValue(null, attribute);
    438                                 } else {
    439                                     mActiveTextDepthMask[i].set(currentNodeDepth, true);
    440                                 }
    441                             }
    442                         }
    443                     }
    444 
    445                 } else if (eventType == XmlPullParser.END_TAG) {
    446                     // Pop last node from path
    447                     final int length = pathLengthStack.pop();
    448                     path.setLength(length);
    449 
    450                     if (currentNodeDepth >= 0) {
    451                         if (currentNodeDepth == 0) {
    452                             // Leaving a selection matching node: add a new row with results
    453                             addRow(mCurrentValues);
    454                         } else {
    455                             for (int i = 0; i < mNumberOfProjections; i++) {
    456                                 mActiveTextDepthMask[i].set(currentNodeDepth, false);
    457                             }
    458                         }
    459                         currentNodeDepth--;
    460                     }
    461 
    462                 } else if ((eventType == XmlPullParser.TEXT) && (!parser.isWhitespace())) {
    463                     for (int i = 0; i < mNumberOfProjections; i++) {
    464                         if ((currentNodeDepth >= 0) &&
    465                             (mActiveTextDepthMask[i].get(currentNodeDepth))) {
    466                             mCurrentValues[i + 1] += parser.getText();
    467                         }
    468                     }
    469                 }
    470 
    471                 eventType = parser.next();
    472             }
    473         }
    474     }
    475 }
    476