2 * This document is a part of the source code and related artifacts
3 * for CollectionSpace, an open source collections management system
4 * for museums and related institutions:
6 * http://www.collectionspace.org
7 * http://wiki.collectionspace.org
9 * Copyright 2009 University of California at Berkeley
11 * Licensed under the Educational Community License (ECL), Version 2.0.
12 * You may not use this file except in compliance with this License.
14 * You may obtain a copy of the ECL 2.0 License at
16 * https://source.collectionspace.org/collection-space/LICENSE.txt
18 * Unless required by applicable law or agreed to in writing, software
19 * distributed under the License is distributed on an "AS IS" BASIS,
20 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 * See the License for the specific language governing permissions and
22 * limitations under the License.
24 package org.collectionspace.services.nuxeo.client.java;
26 import java.io.InputStream;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.List;
31 import java.util.Map.Entry;
34 import javax.ws.rs.WebApplicationException;
35 import javax.ws.rs.core.MediaType;
36 import javax.ws.rs.core.Response;
38 import org.collectionspace.services.authorization.AccountPermission;
39 import org.collectionspace.services.jaxb.AbstractCommonList;
40 import org.collectionspace.services.client.PayloadInputPart;
41 import org.collectionspace.services.client.PayloadOutputPart;
42 import org.collectionspace.services.client.PoxPayloadIn;
43 import org.collectionspace.services.client.PoxPayloadOut;
44 import org.collectionspace.services.client.workflow.WorkflowClient;
45 import org.collectionspace.services.common.authorityref.AuthorityRefList;
46 import org.collectionspace.services.common.authorization_mgt.AuthorizationCommon;
47 import org.collectionspace.services.common.context.JaxRsContext;
48 import org.collectionspace.services.common.context.MultipartServiceContext;
49 import org.collectionspace.services.common.context.ServiceContext;
50 import org.collectionspace.services.common.document.BadRequestException;
51 import org.collectionspace.services.common.document.DocumentNotFoundException;
52 import org.collectionspace.services.common.document.DocumentUtils;
53 import org.collectionspace.services.common.document.DocumentWrapper;
54 import org.collectionspace.services.common.document.DocumentFilter;
55 import org.collectionspace.services.common.document.DocumentHandler.Action;
56 import org.collectionspace.services.common.security.SecurityUtils;
57 import org.collectionspace.services.common.security.UnauthorizedException;
58 import org.collectionspace.services.common.service.ObjectPartType;
59 import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
60 import org.collectionspace.services.common.vocabulary.RefNameUtils;
61 import org.dom4j.Element;
63 import org.jboss.resteasy.plugins.providers.multipart.InputPart;
64 import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
66 import org.nuxeo.ecm.core.api.DocumentModel;
67 import org.nuxeo.ecm.core.api.DocumentModelList;
68 import org.nuxeo.ecm.core.api.model.Property;
69 import org.nuxeo.ecm.core.api.model.PropertyException;
71 import org.nuxeo.ecm.core.schema.types.Schema;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
75 import org.dom4j.Document;
78 * RemoteDocumentModelHandler
80 * $LastChangedRevision: $
85 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
86 extends DocumentModelHandler<T, TL> {
89 private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
92 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
95 public void setServiceContext(ServiceContext ctx) { //FIXME: Apply proper generics to ServiceContext<PoxPayloadIn, PoxPayloadOut>
96 if (ctx instanceof MultipartServiceContext) {
97 super.setServiceContext(ctx);
99 throw new IllegalArgumentException("setServiceContext requires instance of "
100 + MultipartServiceContext.class.getName());
105 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
108 public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
109 DocumentModel docModel = wrapDoc.getWrappedObject();
110 //return at least those document part(s) that were received
111 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
112 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
113 PoxPayloadIn input = ctx.getInput();
115 List<PayloadInputPart> inputParts = ctx.getInput().getParts();
116 for (PayloadInputPart part : inputParts) {
117 String partLabel = part.getLabel();
118 ObjectPartType partMeta = partsMetaMap.get(partLabel);
119 // extractPart(docModel, partLabel, partMeta);
120 Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
121 addOutputPart(unQObjectProperties, partLabel, partMeta);
124 if (logger.isWarnEnabled() == true) {
125 logger.warn("MultipartInput part was null for document id = " +
132 * Adds the output part.
134 * @param unQObjectProperties the un q object properties
135 * @param schema the schema
136 * @param partMeta the part meta
137 * @throws Exception the exception
139 protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
141 Element doc = DocumentUtils.buildDocument(partMeta, schema,
142 unQObjectProperties);
143 if (logger.isDebugEnabled() == true) {
144 logger.debug(doc.asXML());
146 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
147 ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
151 * Extract paging info.
153 * @param commonsList the commons list
155 * @throws Exception the exception
157 public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
159 AbstractCommonList commonList = (AbstractCommonList) theCommonList;
161 DocumentFilter docFilter = this.getDocumentFilter();
162 long pageSize = docFilter.getPageSize();
163 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
164 // set the page size and page number
165 commonList.setPageNum(pageNum);
166 commonList.setPageSize(pageSize);
167 DocumentModelList docList = wrapDoc.getWrappedObject();
168 // Set num of items in list. this is useful to our testing framework.
169 commonList.setItemsInPage(docList.size());
170 // set the total result size
171 commonList.setTotalItems(docList.totalSize());
173 return (TL) commonList;
177 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
180 public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
183 DocumentModel docModel = wrapDoc.getWrappedObject();
184 String[] schemas = docModel.getDeclaredSchemas();
185 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
186 for (String schema : schemas) {
187 ObjectPartType partMeta = partsMetaMap.get(schema);
188 if (partMeta == null) {
189 continue; // unknown part, ignore
191 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
192 addOutputPart(unQObjectProperties, schema, partMeta);
194 addAccountPermissionsPart();
197 private void addAccountPermissionsPart() throws Exception {
198 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
199 String currentServiceName = ctx.getServiceName();
200 String workflowSubResource = "/";
201 JaxRsContext jaxRsContext = ctx.getJaxRsContext();
202 if (jaxRsContext != null) {
203 String resourceName = SecurityUtils.getResourceName(jaxRsContext.getUriInfo());
204 workflowSubResource = workflowSubResource + resourceName + WorkflowClient.SERVICE_PATH + "/";
206 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
208 AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
209 currentServiceName, workflowSubResource);
210 PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", accountPermission);
211 ctx.addOutputPart(accountPermissionPart);
215 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
218 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
220 //TODO filling extension parts should be dynamic
221 //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
222 //not an ideal way of populating objects.
223 DocumentModel docModel = wrapDoc.getWrappedObject();
224 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
225 PoxPayloadIn input = ctx.getInput();
226 if (input.getParts().isEmpty()) {
227 String msg = "No payload found!";
228 logger.error(msg + "Ctx=" + getServiceContext().toString());
229 throw new BadRequestException(msg);
232 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
234 //iterate over parts received and fill those parts
235 List<PayloadInputPart> inputParts = input.getParts();
236 for (PayloadInputPart part : inputParts) {
238 String partLabel = part.getLabel();
239 if (partLabel == null) {
240 String msg = "Part label is missing or empty!";
241 logger.error(msg + "Ctx=" + getServiceContext().toString());
242 throw new BadRequestException(msg);
245 //skip if the part is not in metadata
246 ObjectPartType partMeta = partsMetaMap.get(partLabel);
247 if (partMeta == null) {
250 fillPart(part, docModel, partMeta, action, ctx);
256 * fillPart fills an XML part into given document model
257 * @param part to fill
258 * @param docModel for the given object
259 * @param partMeta metadata for the object to fill
262 protected void fillPart(PayloadInputPart part, DocumentModel docModel,
263 ObjectPartType partMeta, Action action, ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx)
265 //check if this is an xml part
266 if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
267 // Document document = DocumentUtils.parseDocument(payload, partMeta,
268 // false /*don't validate*/);
269 Element element = part.getElementBody();
270 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
271 if (action == Action.UPDATE) {
272 this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
274 docModel.setProperties(partMeta.getLabel(), objectProps);
279 * Filters out read only properties, so they cannot be set on update.
280 * TODO: add configuration support to do this generally
281 * @param objectProps the properties parsed from the update payload
282 * @param partMeta metadata for the object to fill
284 public void filterReadOnlyPropertiesForPart(
285 Map<String, Object> objectProps, ObjectPartType partMeta) {
286 // Currently a no-op, but can be overridden in Doc handlers.
290 * extractPart extracts an XML object from given DocumentModel
292 * @param schema of the object to extract
293 * @param partMeta metadata for the object to extract
296 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
298 return extractPart(docModel, schema, partMeta, null);
302 * extractPart extracts an XML object from given DocumentModel
304 * @param schema of the object to extract
305 * @param partMeta metadata for the object to extract
308 protected Map<String, Object> extractPart(
309 DocumentModel docModel, String schema, ObjectPartType partMeta,
310 Map<String, Object> addToMap)
312 Map<String, Object> result = null;
314 MediaType mt = MediaType.valueOf(partMeta.getContent().getContentType()); //FIXME: REM - This is no longer needed. Everything is POX
315 if (mt.equals(MediaType.APPLICATION_XML_TYPE)) {
316 Map<String, Object> objectProps = docModel.getProperties(schema);
317 //unqualify properties before sending the doc over the wire (to save bandwidh)
318 //FIXME: is there a better way to avoid duplication of a Map/Collection?
319 Map<String, Object> unQObjectProperties =
320 (addToMap != null) ? addToMap : (new HashMap<String, Object>());
321 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
322 for (Entry<String, Object> entry : qualifiedEntries) {
323 String unqProp = getUnQProperty(entry.getKey());
324 unQObjectProperties.put(unqProp, entry.getValue());
326 result = unQObjectProperties;
327 } //TODO: handle other media types
333 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
336 public AuthorityRefList getAuthorityRefs(
337 DocumentWrapper<DocumentModel> docWrapper,
338 List<String> authRefFieldNames) throws PropertyException {
340 AuthorityRefList authRefList = new AuthorityRefList();
341 AbstractCommonList commonList = (AbstractCommonList) authRefList;
343 DocumentFilter docFilter = this.getDocumentFilter();
344 long pageSize = docFilter.getPageSize();
345 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
346 // set the page size and page number
347 commonList.setPageNum(pageNum);
348 commonList.setPageSize(pageSize);
350 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
351 DocumentModel docModel = docWrapper.getWrappedObject();
354 int iFirstToUse = (int)(pageSize*pageNum);
355 int nFoundInPage = 0;
357 for (String authRefFieldName : authRefFieldNames) {
359 // FIXME: Can use the schema to validate field existence,
360 // to help avoid encountering PropertyExceptions.
361 String schemaName = DocumentUtils.getSchemaNamePart(authRefFieldName);
362 Schema schema = DocumentUtils.getSchemaFromName(schemaName);
364 String descendantAuthRefFieldName = DocumentUtils.getDescendantAuthRefFieldName(authRefFieldName);
365 if (descendantAuthRefFieldName != null && !descendantAuthRefFieldName.trim().isEmpty()) {
366 authRefFieldName = DocumentUtils.getAncestorAuthRefFieldName(authRefFieldName);
369 String xpath = "//" + authRefFieldName;
370 Property prop = docModel.getProperty(xpath);
375 // If this is a single scalar field, with no children,
376 // add an item with its values to the authRefs list.
377 if (DocumentUtils.isSimpleType(prop)) {
378 String refName = prop.getValue(String.class);
379 if (refName == null) {
382 refName = refName.trim();
383 if (refName.isEmpty()) {
386 if((nFoundTotal < iFirstToUse)
387 || (nFoundInPage >= pageSize)) {
393 appendToAuthRefsList(refName, schemaName, authRefFieldName, list);
395 // Otherwise, if this field has children, cycle through each child.
397 // Whenever we find instances of the descendant field among
398 // these children, add an item with its values to the authRefs list.
400 // FIXME: When we increase maximum repeatability depth, that is, the depth
401 // between ancestor and descendant, we'll need to use recursion here,
402 // rather than making fixed assumptions about hierarchical depth.
403 } else if ((DocumentUtils.isListType(prop) || DocumentUtils.isComplexType(prop))
404 && prop.size() > 0) {
406 Collection<Property> childProp = prop.getChildren();
407 for (Property cProp : childProp) {
408 if (DocumentUtils.isSimpleType(cProp) && cProp.getName().equals(descendantAuthRefFieldName)) {
409 String refName = cProp.getValue(String.class);
410 if (refName == null) {
413 refName = refName.trim();
414 if (refName.isEmpty()) {
417 if((nFoundTotal < iFirstToUse)
418 || (nFoundInPage >= pageSize)) {
424 appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
425 } else if ((DocumentUtils.isListType(cProp) || DocumentUtils.isComplexType(cProp))
426 && prop.size() > 0) {
427 Collection<Property> grandChildProp = cProp.getChildren();
428 for (Property gProp : grandChildProp) {
429 if (DocumentUtils.isSimpleType(gProp) && gProp.getName().equals(descendantAuthRefFieldName)) {
430 String refName = gProp.getValue(String.class);
431 if (refName == null) {
434 refName = refName.trim();
435 if (refName.isEmpty()) {
438 if((nFoundTotal < iFirstToUse)
439 || (nFoundInPage >= pageSize)) {
445 appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
452 // Set num of items in list. this is useful to our testing framework.
453 commonList.setItemsInPage(nFoundInPage);
454 // set the total result size
455 commonList.setTotalItems(nFoundTotal);
457 } catch (PropertyException pe) {
458 String msg = "Attempted to retrieve value for invalid or missing authority field. "
459 + "Check authority field properties in tenant bindings.";
460 logger.warn(msg, pe);
462 } catch (Exception e) {
463 if (logger.isDebugEnabled()) {
464 logger.debug("Caught exception in getAuthorityRefs", e);
466 Response response = Response.status(
467 Response.Status.INTERNAL_SERVER_ERROR).entity(
468 "Failed to retrieve authority references").type(
469 "text/plain").build();
470 throw new WebApplicationException(response);
476 private void appendToAuthRefsList(String refName, String schemaName,
477 String fieldName, List<AuthorityRefList.AuthorityRefItem> list)
479 if (DocumentUtils.getSchemaNamePart(fieldName).isEmpty()) {
480 fieldName = DocumentUtils.appendSchemaName(schemaName, fieldName);
482 list.add(authorityRefListItem(fieldName, refName));
485 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
487 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
489 RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
490 ilistItem.setRefName(refName);
491 ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
492 ilistItem.setItemDisplayName(termInfo.displayName);
493 ilistItem.setSourceField(authRefFieldName);
494 ilistItem.setUri(termInfo.getRelativeUri());
495 } catch (Exception e) {
496 // Do nothing upon encountering an Exception here.
502 * Returns the primary value from a list of values.
504 * Assumes that the first value is the primary value.
505 * This assumption may change when and if the primary value
506 * is identified explicitly.
508 * @param values a list of values.
509 * @param propertyName the name of a property through
510 * which the value can be extracted.
511 * @return the primary value.
512 protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
513 String primaryValue = "";
514 if (values == null || values.size() == 0) {
517 Object value = values.get(0);
518 if (value instanceof String) {
520 primaryValue = (String) value;
522 // Multivalue group of fields
523 } else if (value instanceof Map) {
525 Map map = (Map) value;
526 if (map.values().size() > 0) {
527 if (map.get(propertyName) != null) {
528 primaryValue = (String) map.get(propertyName);
533 logger.warn("Unexpected type for property " + propertyName
534 + " in multivalue list: not String or Map.");
541 * Gets a simple property from the document.
543 * For completeness, as this duplicates DocumentModel method.
545 * @param docModel The document model to get info from
546 * @param schema The name of the schema (part)
547 * @param propertyName The simple scalar property type
548 * @return property value as String
550 protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
551 String xpath = "/"+schema+":"+propName;
553 return (String)docModel.getPropertyValue(xpath);
554 } catch(PropertyException pe) {
555 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
556 +pe.getLocalizedMessage());
557 } catch(ClassCastException cce) {
558 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
559 +cce.getLocalizedMessage());
560 } catch(Exception e) {
561 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
562 +e.getLocalizedMessage());
567 * Gets first of a repeating list of scalar values, as a String, from the document.
569 * @param docModel The document model to get info from
570 * @param schema The name of the schema (part)
571 * @param listName The name of the scalar list property
572 * @return first value in list, as a String, or empty string if the list is empty
574 protected String getFirstRepeatingStringProperty(
575 DocumentModel docModel, String schema, String listName) {
576 String xpath = "/"+schema+":"+listName+"/[0]";
578 return (String)docModel.getPropertyValue(xpath);
579 } catch(PropertyException pe) {
580 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a repeating scalar?"
581 +pe.getLocalizedMessage());
582 } catch(IndexOutOfBoundsException ioobe) {
583 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
584 return ""; // gracefully handle missing elements
585 } catch(ClassCastException cce) {
586 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a repeating String property?"
587 +cce.getLocalizedMessage());
588 } catch(Exception e) {
589 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
590 +e.getLocalizedMessage());
596 * Gets first of a repeating list of scalar values, as a String, from the document.
598 * @param docModel The document model to get info from
599 * @param schema The name of the schema (part)
600 * @param listName The name of the scalar list property
601 * @return first value in list, as a String, or empty string if the list is empty
603 protected String getStringValueInPrimaryRepeatingComplexProperty(
604 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
605 String xpath = "/"+schema+":"+complexPropertyName+"/[0]/"+fieldName;
607 return (String)docModel.getPropertyValue(xpath);
608 } catch(PropertyException pe) {
609 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
610 +pe.getLocalizedMessage());
611 } catch(IndexOutOfBoundsException ioobe) {
612 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
613 return ""; // gracefully handle missing elements
614 } catch(ClassCastException cce) {
615 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
616 +cce.getLocalizedMessage());
617 } catch(Exception e) {
618 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
619 +e.getLocalizedMessage());
624 * Gets XPath value from schema. Note that only "/" and "[n]" are
625 * supported for xpath. Can omit grouping elements for repeating complex types,
626 * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
627 * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
628 * If there are no entries for a list of scalars or for a list of complex types,
629 * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
630 * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
631 * that many elements in the list.
632 * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
634 * @param docModel The document model to get info from
635 * @param schema The name of the schema (part)
636 * @param xpath The XPath expression (without schema prefix)
637 * @return value the indicated property value as a String
639 protected static String getXPathStringValue(DocumentModel docModel, String schema, String xpath) {
640 xpath = schema+":"+xpath;
642 return (String)docModel.getPropertyValue(xpath);
643 } catch(PropertyException pe) {
644 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad XPath spec?"
645 +pe.getLocalizedMessage());
646 } catch(ClassCastException cce) {
647 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
648 +cce.getLocalizedMessage());
649 } catch(IndexOutOfBoundsException ioobe) {
650 // Nuxeo seems to handle foo/[0]/bar when it is missing,
651 // but not foo/bar[0] (for repeating scalars).
652 if(xpath.endsWith("[0]")) { // gracefully handle missing elements
655 String msg = ioobe.getMessage();
656 if(msg!=null && msg.equals("Index: 0, Size: 0")) {
657 // Some other variant on a missing sub-field; quietly absorb.
659 } // Otherwise, e.g., for true OOB indices, propagate the exception.
661 throw new RuntimeException("Problem retrieving property {"+xpath+"}:"
662 +ioobe.getLocalizedMessage());
663 } catch(Exception e) {
664 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
665 +e.getLocalizedMessage());