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
17package com.example.android.xmladapters;
18
19import org.apache.http.HttpEntity;
20import org.apache.http.HttpResponse;
21import org.apache.http.HttpStatus;
22import org.apache.http.client.methods.HttpGet;
23
24import android.content.ContentProvider;
25import android.content.ContentResolver;
26import android.content.ContentValues;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.res.Resources;
29import android.database.Cursor;
30import android.database.MatrixCursor;
31import android.net.Uri;
32import android.net.http.AndroidHttpClient;
33import android.text.TextUtils;
34import android.util.Log;
35import android.widget.CursorAdapter;
36
37import org.xmlpull.v1.XmlPullParser;
38import org.xmlpull.v1.XmlPullParserException;
39import org.xmlpull.v1.XmlPullParserFactory;
40
41import java.io.FileNotFoundException;
42import java.io.IOException;
43import java.io.InputStream;
44import java.util.BitSet;
45import java.util.List;
46import java.util.Stack;
47import 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 */
127public 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