]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
4f43642242a409d951f1b703ae9b33b6589c9964
[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.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;
62
63 import org.jboss.resteasy.plugins.providers.multipart.InputPart;
64 import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
65
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;
70
71 import org.nuxeo.ecm.core.schema.types.Schema;
72
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
75 import org.dom4j.Document;
76
77 /**
78  * RemoteDocumentModelHandler
79  *
80  * $LastChangedRevision: $
81  * $LastChangedDate: $
82  * @param <T> 
83  * @param <TL> 
84  */
85 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
86         extends DocumentModelHandler<T, TL> {
87
88     /** The logger. */
89     private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
90
91     /* (non-Javadoc)
92      * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
93      */
94     @Override
95     public void setServiceContext(ServiceContext ctx) {  //FIXME: Apply proper generics to ServiceContext<PoxPayloadIn, PoxPayloadOut>
96         if (ctx instanceof MultipartServiceContext) {
97             super.setServiceContext(ctx);
98         } else {
99             throw new IllegalArgumentException("setServiceContext requires instance of "
100                     + MultipartServiceContext.class.getName());
101         }
102     }
103
104     /* (non-Javadoc)
105      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
106      */
107     @Override
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();
114         if (input != null) {
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);
122                 }
123         } else {
124                 if (logger.isWarnEnabled() == true) {
125                         logger.warn("MultipartInput part was null for document id = " +
126                                         docModel.getName());
127                 }
128         }
129     }
130
131     /**
132      * Adds the output part.
133      *
134      * @param unQObjectProperties the un q object properties
135      * @param schema the schema
136      * @param partMeta the part meta
137      * @throws Exception the exception
138      */
139     protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
140             throws Exception {
141         Element doc = DocumentUtils.buildDocument(partMeta, schema,
142                 unQObjectProperties);
143         if (logger.isDebugEnabled() == true) {
144             logger.debug(doc.asXML());
145         }
146         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
147         ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
148     }
149
150     /**
151      * Extract paging info.
152      *
153      * @param commonsList the commons list
154      * @return the tL
155      * @throws Exception the exception
156      */
157     public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
158             throws Exception {
159         AbstractCommonList commonList = (AbstractCommonList) theCommonList;
160
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());
172
173         return (TL) commonList;
174     }
175
176     /* (non-Javadoc)
177      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
178      */
179     @Override
180     public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
181             throws Exception {
182
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
190             }
191             Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
192             addOutputPart(unQObjectProperties, schema, partMeta);
193         }
194         addAccountPermissionsPart();
195     }
196     
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 + "/";
205         } else {
206                 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
207         }
208         AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
209                         currentServiceName, workflowSubResource);
210         PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", accountPermission);
211         ctx.addOutputPart(accountPermissionPart);
212     }
213
214     /* (non-Javadoc)
215      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
216      */
217     @Override
218     public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
219
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);
230         }
231
232         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
233
234         //iterate over parts received and fill those parts
235         List<PayloadInputPart> inputParts = input.getParts();
236         for (PayloadInputPart part : inputParts) {
237
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);
243             }
244
245             //skip if the part is not in metadata
246             ObjectPartType partMeta = partsMetaMap.get(partLabel);
247             if (partMeta == null) {
248                 continue;
249             }
250             fillPart(part, docModel, partMeta, action, ctx);
251         }//rof
252
253     }
254
255     /**
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
260      * @throws Exception
261      */
262     protected void fillPart(PayloadInputPart part, DocumentModel docModel,
263             ObjectPartType partMeta, Action action, ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx)
264             throws Exception {
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);
273                 }
274                 docModel.setProperties(partMeta.getLabel(), objectProps);
275             }
276         }
277
278     /**
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
283      */
284     public void filterReadOnlyPropertiesForPart(
285             Map<String, Object> objectProps, ObjectPartType partMeta) {
286         // Currently a no-op, but can be overridden in Doc handlers.
287     }
288
289     /**
290      * extractPart extracts an XML object from given DocumentModel
291      * @param docModel
292      * @param schema of the object to extract
293      * @param partMeta metadata for the object to extract
294      * @throws Exception
295      */
296     protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
297             throws Exception {
298         return extractPart(docModel, schema, partMeta, null);
299     }
300
301     /**
302      * extractPart extracts an XML object from given DocumentModel
303      * @param docModel
304      * @param schema of the object to extract
305      * @param partMeta metadata for the object to extract
306      * @throws Exception
307      */
308     protected Map<String, Object> extractPart(
309             DocumentModel docModel, String schema, ObjectPartType partMeta,
310             Map<String, Object> addToMap)
311             throws Exception {
312         Map<String, Object> result = null;
313
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());
325             }
326             result = unQObjectProperties;
327         } //TODO: handle other media types
328
329         return result;
330     }
331
332     /* (non-Javadoc)
333      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
334      */
335     @Override
336     public AuthorityRefList getAuthorityRefs(
337             DocumentWrapper<DocumentModel> docWrapper,
338             List<String> authRefFieldNames) throws PropertyException {
339
340         AuthorityRefList authRefList = new AuthorityRefList();
341         AbstractCommonList commonList = (AbstractCommonList) authRefList;
342         
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);
349         
350         List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
351         DocumentModel docModel = docWrapper.getWrappedObject();
352
353         try {
354                 int iFirstToUse = (int)(pageSize*pageNum);
355                 int nFoundInPage = 0;
356                 int nFoundTotal = 0;
357             for (String authRefFieldName : authRefFieldNames) {
358
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);
363
364                 String descendantAuthRefFieldName = DocumentUtils.getDescendantAuthRefFieldName(authRefFieldName);
365                 if (descendantAuthRefFieldName != null && !descendantAuthRefFieldName.trim().isEmpty()) {
366                     authRefFieldName = DocumentUtils.getAncestorAuthRefFieldName(authRefFieldName);
367                 }
368
369                 String xpath = "//" + authRefFieldName;
370                 Property prop = docModel.getProperty(xpath);
371                 if (prop == null) {
372                     continue;
373                 }
374
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) {
380                         continue;
381                     }
382                     refName = refName.trim();
383                     if (refName.isEmpty()) {
384                         continue;
385                     }
386                         if((nFoundTotal < iFirstToUse)
387                                 || (nFoundInPage >= pageSize)) {
388                                 nFoundTotal++;
389                                 continue;
390                         }
391                         nFoundTotal++;
392                         nFoundInPage++;
393                         appendToAuthRefsList(refName, schemaName, authRefFieldName, list);
394
395                     // Otherwise, if this field has children, cycle through each child.
396                     //
397                     // Whenever we find instances of the descendant field among
398                     // these children, add an item with its values to the authRefs list.
399                     //
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) {
405                     
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) {
411                                 continue;
412                             }
413                             refName = refName.trim();
414                             if (refName.isEmpty()) {
415                                 continue;
416                             }
417                                 if((nFoundTotal < iFirstToUse)
418                                         || (nFoundInPage >= pageSize)) {
419                                         nFoundTotal++;
420                                         continue;
421                                 }
422                                 nFoundTotal++;
423                                 nFoundInPage++;
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) {
432                                         continue;
433                                     }
434                                     refName = refName.trim();
435                                     if (refName.isEmpty()) {
436                                         continue;
437                                     }
438                                         if((nFoundTotal < iFirstToUse)
439                                                 || (nFoundInPage >= pageSize)) {
440                                                 nFoundTotal++;
441                                                 continue;
442                                         }
443                                         nFoundTotal++;
444                                         nFoundInPage++;
445                                     appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
446                                 }
447                             }
448                         }
449                     }
450                 }
451             }
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);
456             
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);
461             throw pe;
462         } catch (Exception e) {
463             if (logger.isDebugEnabled()) {
464                 logger.debug("Caught exception in getAuthorityRefs", e);
465             }
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);
471         }
472
473         return authRefList;
474     }
475
476     private void appendToAuthRefsList(String refName, String schemaName,
477             String fieldName, List<AuthorityRefList.AuthorityRefItem> list)
478             throws Exception {
479         if (DocumentUtils.getSchemaNamePart(fieldName).isEmpty()) {
480             fieldName = DocumentUtils.appendSchemaName(schemaName, fieldName);
481         }
482         list.add(authorityRefListItem(fieldName, refName));
483     }
484
485     private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
486
487         AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
488         try {
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.
497         }
498         return ilistItem;
499     }
500
501     /**
502      * Returns the primary value from a list of values.
503      *
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.
507      *
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) {
515             return primaryValue;
516         }
517         Object value = values.get(0);
518         if (value instanceof String) {
519             if (value != null) {
520                 primaryValue = (String) value;
521             }
522        // Multivalue group of fields
523        } else if (value instanceof Map) {
524             if (value != null) {
525                 Map map = (Map) value;
526                 if (map.values().size() > 0) {
527                     if (map.get(propertyName) != null) {
528                       primaryValue = (String) map.get(propertyName);
529                     }
530                 }
531             }
532        } else {
533             logger.warn("Unexpected type for property " + propertyName
534                     + " in multivalue list: not String or Map.");
535        }
536        return primaryValue;
537     }
538      */
539
540     /**
541      * Gets a simple property from the document.
542      *
543      * For completeness, as this duplicates DocumentModel method. 
544      *
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
549      */
550     protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
551         String xpath = "/"+schema+":"+propName;
552         try {
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());
563         }
564     }
565
566     /**
567      * Gets first of a repeating list of scalar values, as a String, from the document.
568      *
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
573      */
574     protected String getFirstRepeatingStringProperty(
575                 DocumentModel docModel, String schema, String listName) {
576         String xpath = "/"+schema+":"+listName+"/[0]";
577         try {
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());
591         }
592     }
593    
594
595     /**
596      * Gets first of a repeating list of scalar values, as a String, from the document.
597      *
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
602      */
603     protected String getStringValueInPrimaryRepeatingComplexProperty(
604                 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
605         String xpath = "/"+schema+":"+complexPropertyName+"/[0]/"+fieldName;
606         try {
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());
620         }
621     }
622    
623     /**
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.
633      *
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
638      */
639     protected static String getXPathStringValue(DocumentModel docModel, String schema, String xpath) {
640         xpath = schema+":"+xpath;
641         try {
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
653                         return "";
654                 } else {
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.
658                                 return "";
659                         } // Otherwise, e.g., for true OOB indices, propagate the exception.
660                 }
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());
666         }
667     }
668    
669 }