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