]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
829f7f9c0703ada0488ce423dabb162d4cf1ad1c
[tmp/jakarta-migration.git] /
1 /**
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:
5
6  *  http://www.collectionspace.org
7  *  http://wiki.collectionspace.org
8
9  *  Copyright 2009 University of California at Berkeley
10
11  *  Licensed under the Educational Community License (ECL), Version 2.0.
12  *  You may not use this file except in compliance with this License.
13
14  *  You may obtain a copy of the ECL 2.0 License at
15
16  *  https://source.collectionspace.org/collection-space/LICENSE.txt
17
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.
23  */
24 package org.collectionspace.services.nuxeo.client.java;
25
26 import java.io.InputStream;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Map.Entry;
32 import java.util.Set;
33
34 import javax.ws.rs.WebApplicationException;
35 import javax.ws.rs.core.MediaType;
36 import javax.ws.rs.core.Response;
37
38 import org.collectionspace.services.jaxb.AbstractCommonList;
39 import org.collectionspace.services.common.authorityref.AuthorityRefList;
40 import org.collectionspace.services.common.context.MultipartServiceContext;
41 import org.collectionspace.services.common.context.ServiceContext;
42 import org.collectionspace.services.common.document.BadRequestException;
43 import org.collectionspace.services.common.document.DocumentUtils;
44 import org.collectionspace.services.common.document.DocumentWrapper;
45 import org.collectionspace.services.common.document.DocumentFilter;
46 import org.collectionspace.services.common.document.DocumentHandler.Action;
47 import org.collectionspace.services.common.service.ObjectPartType;
48 import org.collectionspace.services.common.vocabulary.RefNameUtils;
49
50 import org.jboss.resteasy.plugins.providers.multipart.InputPart;
51 import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
52
53 import org.nuxeo.ecm.core.api.DocumentModel;
54 import org.nuxeo.ecm.core.api.DocumentModelList;
55 import org.nuxeo.ecm.core.api.model.Property;
56 import org.nuxeo.ecm.core.api.model.PropertyException;
57
58 import org.nuxeo.ecm.core.schema.types.Schema;
59
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62 import org.w3c.dom.Document;
63
64 /**
65  * RemoteDocumentModelHandler
66  *
67  * $LastChangedRevision: $
68  * $LastChangedDate: $
69  * @param <T> 
70  * @param <TL> 
71  */
72 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
73         extends DocumentModelHandler<T, TL> {
74
75     /** The logger. */
76     private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
77
78     /* (non-Javadoc)
79      * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
80      */
81     @Override
82     public void setServiceContext(ServiceContext ctx) {  //FIXME: Apply proper generics to ServiceContext<MultipartInput, MultipartOutput>
83         if (ctx instanceof MultipartServiceContext) {
84             super.setServiceContext(ctx);
85         } else {
86             throw new IllegalArgumentException("setServiceContext requires instance of "
87                     + MultipartServiceContext.class.getName());
88         }
89     }
90
91     /* (non-Javadoc)
92      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
93      */
94     @Override
95     public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
96         DocumentModel docModel = wrapDoc.getWrappedObject();
97         //return at least those document part(s) that were received
98         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
99         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
100         MultipartInput input = ctx.getInput();
101         if (input != null) {
102                 List<InputPart> inputParts = ctx.getInput().getParts();
103                 for (InputPart part : inputParts) {
104                     String partLabel = part.getHeaders().getFirst("label");
105                     ObjectPartType partMeta = partsMetaMap.get(partLabel);
106         //            extractPart(docModel, partLabel, partMeta);
107                     Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
108                     addOutputPart(unQObjectProperties, partLabel, partMeta);
109                 }
110         } else {
111                 if (logger.isWarnEnabled() == true) {
112                         logger.warn("MultipartInput part was null for document id = " +
113                                         docModel.getName());
114                 }
115         }
116     }
117
118     /**
119      * Adds the output part.
120      *
121      * @param unQObjectProperties the un q object properties
122      * @param schema the schema
123      * @param partMeta the part meta
124      * @throws Exception the exception
125      */
126     private void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
127             throws Exception {
128         Document doc = DocumentUtils.buildDocument(partMeta, schema,
129                 unQObjectProperties);
130         if (logger.isDebugEnabled() == true) {
131             logger.debug(DocumentUtils.xmlToString(doc));
132         }
133         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
134         ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
135     }
136
137     /**
138      * Extract paging info.
139      *
140      * @param commonsList the commons list
141      * @return the tL
142      * @throws Exception the exception
143      */
144     public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
145             throws Exception {
146         AbstractCommonList commonList = (AbstractCommonList) theCommonList;
147
148         DocumentFilter docFilter = this.getDocumentFilter();
149         long pageSize = docFilter.getPageSize();
150         long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
151         // set the page size and page number
152         commonList.setPageNum(pageNum);
153         commonList.setPageSize(pageSize);
154         DocumentModelList docList = wrapDoc.getWrappedObject();
155         // Set num of items in list. this is useful to our testing framework.
156         commonList.setItemsInPage(docList.size());
157         // set the total result size
158         commonList.setTotalItems(docList.totalSize());
159
160         return (TL) commonList;
161     }
162
163     /* (non-Javadoc)
164      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
165      */
166     @Override
167     public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
168             throws Exception {
169
170         DocumentModel docModel = wrapDoc.getWrappedObject();
171         String[] schemas = docModel.getDeclaredSchemas();
172         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
173         for (String schema : schemas) {
174             ObjectPartType partMeta = partsMetaMap.get(schema);
175             if (partMeta == null) {
176                 continue; // unknown part, ignore
177             }
178             Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
179             addOutputPart(unQObjectProperties, schema, partMeta);
180         }
181     }
182
183     /* (non-Javadoc)
184      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
185      */
186     @Override
187     public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
188
189         //TODO filling extension parts should be dynamic
190         //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
191         //not an ideal way of populating objects.
192         DocumentModel docModel = wrapDoc.getWrappedObject();
193         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
194         MultipartInput input = ctx.getInput();
195         if (input.getParts().isEmpty()) {
196             String msg = "No payload found!";
197             logger.error(msg + "Ctx=" + getServiceContext().toString());
198             throw new BadRequestException(msg);
199         }
200
201         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
202
203         //iterate over parts received and fill those parts
204         List<InputPart> inputParts = input.getParts();
205         for (InputPart part : inputParts) {
206
207             String partLabel = part.getHeaders().getFirst("label");
208             if (partLabel == null) {
209                 String msg = "Part label is missing or empty!";
210                 logger.error(msg + "Ctx=" + getServiceContext().toString());
211                 throw new BadRequestException(msg);
212             }
213
214             //skip if the part is not in metadata
215             ObjectPartType partMeta = partsMetaMap.get(partLabel);
216             if (partMeta == null) {
217                 continue;
218             }
219             fillPart(part, docModel, partMeta, action, ctx);
220         }//rof
221
222     }
223
224     /**
225      * fillPart fills an XML part into given document model
226      * @param part to fill
227      * @param docModel for the given object
228      * @param partMeta metadata for the object to fill
229      * @throws Exception
230      */
231     protected void fillPart(InputPart part, DocumentModel docModel,
232             ObjectPartType partMeta, Action action, ServiceContext ctx)
233             throws Exception {
234         InputStream payload = part.getBody(InputStream.class, null);
235
236         //check if this is an xml part
237         if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
238             if (payload != null) {
239                 Document document = DocumentUtils.parseDocument(payload, partMeta,
240                         false /*don't validate*/);
241                 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, document, ctx);
242                 if (action == Action.UPDATE) {
243                     this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
244                 }
245                 docModel.setProperties(partMeta.getLabel(), objectProps);
246             }
247         }
248     }
249
250     /**
251      * Filters out read only properties, so they cannot be set on update.
252      * TODO: add configuration support to do this generally
253      * @param objectProps the properties parsed from the update payload
254      * @param partMeta metadata for the object to fill
255      */
256     public void filterReadOnlyPropertiesForPart(
257             Map<String, Object> objectProps, ObjectPartType partMeta) {
258         // Currently a no-op, but can be overridden in Doc handlers.
259     }
260
261     /**
262      * extractPart extracts an XML object from given DocumentModel
263      * @param docModel
264      * @param schema of the object to extract
265      * @param partMeta metadata for the object to extract
266      * @throws Exception
267      */
268     protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
269             throws Exception {
270         return extractPart(docModel, schema, partMeta, null);
271     }
272
273     /**
274      * extractPart extracts an XML object from given DocumentModel
275      * @param docModel
276      * @param schema of the object to extract
277      * @param partMeta metadata for the object to extract
278      * @throws Exception
279      */
280     protected Map<String, Object> extractPart(
281             DocumentModel docModel, String schema, ObjectPartType partMeta,
282             Map<String, Object> addToMap)
283             throws Exception {
284         Map<String, Object> result = null;
285
286         MediaType mt = MediaType.valueOf(partMeta.getContent().getContentType());
287         if (mt.equals(MediaType.APPLICATION_XML_TYPE)) {
288             Map<String, Object> objectProps = docModel.getProperties(schema);
289             //unqualify properties before sending the doc over the wire (to save bandwidh)
290             //FIXME: is there a better way to avoid duplication of a collection?
291             Map<String, Object> unQObjectProperties =
292                     (addToMap != null) ? addToMap : (new HashMap<String, Object>());
293             Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
294             for (Entry<String, Object> entry : qualifiedEntries) {
295                 String unqProp = getUnQProperty(entry.getKey());
296                 unQObjectProperties.put(unqProp, entry.getValue());
297             }
298             result = unQObjectProperties;
299         } //TODO: handle other media types
300
301         return result;
302     }
303
304     /* (non-Javadoc)
305      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
306      */
307     @Override
308     public AuthorityRefList getAuthorityRefs(
309             DocumentWrapper<DocumentModel> docWrapper,
310             List<String> authRefFieldNames) throws PropertyException {
311
312         AuthorityRefList authRefList = new AuthorityRefList();
313         AbstractCommonList commonList = (AbstractCommonList) authRefList;
314         
315         DocumentFilter docFilter = this.getDocumentFilter();
316         long pageSize = docFilter.getPageSize();
317         long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
318         // set the page size and page number
319         commonList.setPageNum(pageNum);
320         commonList.setPageSize(pageSize);
321         
322         List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
323         DocumentModel docModel = docWrapper.getWrappedObject();
324
325         try {
326                 int iFirstToUse = (int)(pageSize*pageNum);
327                 int nFoundInPage = 0;
328                 int nFoundTotal = 0;
329             for (String authRefFieldName : authRefFieldNames) {
330
331                 // FIXME: Can use the schema to validate field existence,
332                 // to help avoid encountering PropertyExceptions.
333                 String schemaName = DocumentUtils.getSchemaNamePart(authRefFieldName);
334                 Schema schema = DocumentUtils.getSchemaFromName(schemaName);
335
336                 String descendantAuthRefFieldName = DocumentUtils.getDescendantAuthRefFieldName(authRefFieldName);
337                 if (descendantAuthRefFieldName != null && !descendantAuthRefFieldName.trim().isEmpty()) {
338                     authRefFieldName = DocumentUtils.getAncestorAuthRefFieldName(authRefFieldName);
339                 }
340
341                 String xpath = "//" + authRefFieldName;
342                 Property prop = docModel.getProperty(xpath);
343                 if (prop == null) {
344                     continue;
345                 }
346
347                 // If this is a single scalar field, with no children,
348                 // add an item with its values to the authRefs list.
349                 if (DocumentUtils.isSimpleType(prop)) {
350                         String refName = prop.getValue(String.class);
351                     if (refName == null) {
352                         continue;
353                     }
354                     refName = refName.trim();
355                     if (refName.isEmpty()) {
356                         continue;
357                     }
358                         if((nFoundTotal < iFirstToUse)
359                                 || (nFoundInPage >= pageSize)) {
360                                 nFoundTotal++;
361                                 continue;
362                         }
363                         nFoundTotal++;
364                         nFoundInPage++;
365                         appendToAuthRefsList(refName, schemaName, authRefFieldName, list);
366
367                     // Otherwise, if this field has children, cycle through each child.
368                     //
369                     // Whenever we find instances of the descendant field among
370                     // these children, add an item with its values to the authRefs list.
371                     //
372                     // FIXME: When we increase maximum repeatability depth, that is, the depth
373                     // between ancestor and descendant, we'll need to use recursion here,
374                     // rather than making fixed assumptions about hierarchical depth.
375                 } else if ((DocumentUtils.isListType(prop) || DocumentUtils.isComplexType(prop))
376                         && prop.size() > 0) {
377                     
378                     Collection<Property> childProp = prop.getChildren();
379                     for (Property cProp : childProp) {
380                         if (DocumentUtils.isSimpleType(cProp) && cProp.getName().equals(descendantAuthRefFieldName)) {
381                                 String refName = cProp.getValue(String.class);
382                             if (refName == null) {
383                                 continue;
384                             }
385                             refName = refName.trim();
386                             if (refName.isEmpty()) {
387                                 continue;
388                             }
389                                 if((nFoundTotal < iFirstToUse)
390                                         || (nFoundInPage >= pageSize)) {
391                                         nFoundTotal++;
392                                         continue;
393                                 }
394                                 nFoundTotal++;
395                                 nFoundInPage++;
396                             appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
397                         } else if ((DocumentUtils.isListType(cProp) || DocumentUtils.isComplexType(cProp))
398                             && prop.size() > 0) {
399                             Collection<Property> grandChildProp = cProp.getChildren();
400                             for (Property gProp : grandChildProp) {
401                                 if (DocumentUtils.isSimpleType(gProp) && gProp.getName().equals(descendantAuthRefFieldName)) {
402                                         String refName = gProp.getValue(String.class);
403                                     if (refName == null) {
404                                         continue;
405                                     }
406                                     refName = refName.trim();
407                                     if (refName.isEmpty()) {
408                                         continue;
409                                     }
410                                         if((nFoundTotal < iFirstToUse)
411                                                 || (nFoundInPage >= pageSize)) {
412                                                 nFoundTotal++;
413                                                 continue;
414                                         }
415                                         nFoundTotal++;
416                                         nFoundInPage++;
417                                     appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
418                                 }
419                             }
420                         }
421                     }
422                 }
423             }
424             // Set num of items in list. this is useful to our testing framework.
425             commonList.setItemsInPage(nFoundInPage);
426             // set the total result size
427             commonList.setTotalItems(nFoundTotal);
428             
429         } catch (PropertyException pe) {
430             String msg = "Attempted to retrieve value for invalid or missing authority field. "
431                     + "Check authority field properties in tenant bindings.";
432             logger.warn(msg, pe);
433             throw pe;
434         } catch (Exception e) {
435             if (logger.isDebugEnabled()) {
436                 logger.debug("Caught exception in getAuthorityRefs", e);
437             }
438             Response response = Response.status(
439                     Response.Status.INTERNAL_SERVER_ERROR).entity(
440                     "Failed to retrieve authority references").type(
441                     "text/plain").build();
442             throw new WebApplicationException(response);
443         }
444
445         return authRefList;
446     }
447
448     private void appendToAuthRefsList(String refName, String schemaName,
449             String fieldName, List<AuthorityRefList.AuthorityRefItem> list)
450             throws Exception {
451         if (DocumentUtils.getSchemaNamePart(fieldName).isEmpty()) {
452             fieldName = DocumentUtils.appendSchemaName(schemaName, fieldName);
453         }
454         list.add(authorityRefListItem(fieldName, refName));
455     }
456
457     private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
458
459         AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
460         try {
461             RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
462             ilistItem.setRefName(refName);
463             ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
464             ilistItem.setItemDisplayName(termInfo.displayName);
465             ilistItem.setSourceField(authRefFieldName);
466             ilistItem.setUri(termInfo.getRelativeUri());
467         } catch (Exception e) {
468             // Do nothing upon encountering an Exception here.
469         }
470         return ilistItem;
471     }
472
473     /**
474      * Returns the primary value from a list of values.
475      *
476      * Assumes that the first value is the primary value.
477      * This assumption may change when and if the primary value
478      * is identified explicitly.
479      *
480      * @param values a list of values.
481      * @param propertyName the name of a property through
482      *     which the value can be extracted.
483      * @return the primary value.
484     protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
485         String primaryValue = "";
486         if (values == null || values.size() == 0) {
487             return primaryValue;
488         }
489         Object value = values.get(0);
490         if (value instanceof String) {
491             if (value != null) {
492                 primaryValue = (String) value;
493             }
494        // Multivalue group of fields
495        } else if (value instanceof Map) {
496             if (value != null) {
497                 Map map = (Map) value;
498                 if (map.values().size() > 0) {
499                     if (map.get(propertyName) != null) {
500                       primaryValue = (String) map.get(propertyName);
501                     }
502                 }
503             }
504        } else {
505             logger.warn("Unexpected type for property " + propertyName
506                     + " in multivalue list: not String or Map.");
507        }
508        return primaryValue;
509     }
510      */
511
512     /**
513      * Gets a simple property from the document.
514      *
515      * For completeness, as this duplicates DocumentModel method. 
516      *
517      * @param docModel The document model to get info from
518      * @param schema The name of the schema (part)
519      * @param propertyName The simple scalar property type
520      * @return property value as String
521      */
522     protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
523         String xpath = "/"+schema+":"+propName;
524         try {
525                 return (String)docModel.getPropertyValue(xpath);
526         } catch(PropertyException pe) {
527                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
528                                 +pe.getLocalizedMessage());
529         } catch(ClassCastException cce) {
530                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
531                                 +cce.getLocalizedMessage());
532         } catch(Exception e) {
533                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
534                                 +e.getLocalizedMessage());
535         }
536     }
537
538     /**
539      * Gets first of a repeating list of scalar values, as a String, from the document.
540      *
541      * @param docModel The document model to get info from
542      * @param schema The name of the schema (part)
543      * @param listName The name of the scalar list property
544      * @return first value in list, as a String, or empty string if the list is empty
545      */
546     protected String getFirstRepeatingStringProperty(
547                 DocumentModel docModel, String schema, String listName) {
548         String xpath = "/"+schema+":"+listName+"/[0]";
549         try {
550                 return (String)docModel.getPropertyValue(xpath);
551         } catch(PropertyException pe) {
552                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a repeating scalar?"
553                                 +pe.getLocalizedMessage());
554         } catch(IndexOutOfBoundsException ioobe) {
555                 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
556                 return "";      // gracefully handle missing elements
557         } catch(ClassCastException cce) {
558                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a repeating String property?"
559                                 +cce.getLocalizedMessage());
560         } catch(Exception e) {
561                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
562                                 +e.getLocalizedMessage());
563         }
564     }
565    
566
567     /**
568      * Gets first of a repeating list of scalar values, as a String, from the document.
569      *
570      * @param docModel The document model to get info from
571      * @param schema The name of the schema (part)
572      * @param listName The name of the scalar list property
573      * @return first value in list, as a String, or empty string if the list is empty
574      */
575     protected String getStringValueInPrimaryRepeatingComplexProperty(
576                 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
577         String xpath = "/"+schema+":"+complexPropertyName+"/[0]/"+fieldName;
578         try {
579                 return (String)docModel.getPropertyValue(xpath);
580         } catch(PropertyException pe) {
581                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
582                                 +pe.getLocalizedMessage());
583         } catch(IndexOutOfBoundsException ioobe) {
584                 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
585                 return "";      // gracefully handle missing elements
586         } catch(ClassCastException cce) {
587                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
588                                 +cce.getLocalizedMessage());
589         } catch(Exception e) {
590                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
591                                 +e.getLocalizedMessage());
592         }
593     }
594    
595     /**
596      * Gets XPath value from schema. Note that only "/" and "[n]" are
597      * supported for xpath. Can omit grouping elements for repeating complex types, 
598      * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
599      * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
600      * If there are no entries for a list of scalars or for a list of complex types, 
601      * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
602      * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
603      * that many elements in the list. 
604      * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
605      *
606      * @param docModel The document model to get info from
607      * @param schema The name of the schema (part)
608      * @param xpath The XPath expression (without schema prefix)
609      * @return value the indicated property value as a String
610      */
611     protected static String getXPathStringValue(DocumentModel docModel, String schema, String xpath) {
612         xpath = schema+":"+xpath;
613         try {
614                 return (String)docModel.getPropertyValue(xpath);
615         } catch(PropertyException pe) {
616                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad XPath spec?"
617                                 +pe.getLocalizedMessage());
618         } catch(ClassCastException cce) {
619                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
620                                 +cce.getLocalizedMessage());
621         } catch(IndexOutOfBoundsException ioobe) {
622                 // Nuxeo seems to handle foo/[0]/bar when it is missing,
623                 // but not foo/bar[0] (for repeating scalars).
624                 if(xpath.endsWith("[0]")) {             // gracefully handle missing elements
625                         return "";
626                 } else {
627                         String msg = ioobe.getMessage();
628                         if(msg!=null && msg.equals("Index: 0, Size: 0")) {
629                                 // Some other variant on a missing sub-field; quietly absorb.
630                                 return "";
631                         } // Otherwise, e.g., for true OOB indices, propagate the exception.
632                 }
633                 throw new RuntimeException("Problem retrieving property {"+xpath+"}:"
634                                 +ioobe.getLocalizedMessage());
635         } catch(Exception e) {
636                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
637                                 +e.getLocalizedMessage());
638         }
639     }
640    
641 }