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 * <provider android:name="XmlDocumentProvider" android:authorities="xmldocument" /> 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 * <library> 105 * <book id="EH94"> 106 * <title>The Old Man and the Sea</title> 107 * <author>Ernest Hemingway</author> 108 * </book> 109 * <book id="XX10"> 110 * <title>The Arabian Nights: Tales of 1,001 Nights</title> 111 * </book> 112 * <no-id> 113 * <book> 114 * <title>Animal Farm</title> 115 * <author>George Orwell</author> 116 * </book> 117 * </no-id> 118 * </library> 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