]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
ca7ff53a1c533dbf712f920e5db4b89cf294fdaf
[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.util.ArrayList;
27 import java.util.Collection;
28 import java.util.GregorianCalendar;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.Set;
34
35 import javax.ws.rs.WebApplicationException;
36 import javax.ws.rs.core.MediaType;
37 import javax.ws.rs.core.Response;
38 import javax.xml.bind.JAXBElement;
39
40 import org.collectionspace.services.authorization.AccountPermission;
41 import org.collectionspace.services.jaxb.AbstractCommonList;
42 import org.collectionspace.services.client.PayloadInputPart;
43 import org.collectionspace.services.client.PayloadOutputPart;
44 import org.collectionspace.services.client.PoxPayloadIn;
45 import org.collectionspace.services.client.PoxPayloadOut;
46 import org.collectionspace.services.client.workflow.WorkflowClient;
47 import org.collectionspace.services.common.api.Tools;
48 import org.collectionspace.services.common.authorityref.AuthorityRefList;
49 import org.collectionspace.services.common.context.JaxRsContext;
50 import org.collectionspace.services.common.context.MultipartServiceContext;
51 import org.collectionspace.services.common.context.ServiceContext;
52 import org.collectionspace.services.common.datetime.DateTimeFormatUtils;
53 import org.collectionspace.services.common.document.BadRequestException;
54 import org.collectionspace.services.common.document.DocumentUtils;
55 import org.collectionspace.services.common.document.DocumentWrapper;
56 import org.collectionspace.services.common.document.DocumentFilter;
57 import org.collectionspace.services.common.document.DocumentHandler.Action;
58 import org.collectionspace.services.common.security.SecurityUtils;
59 import org.collectionspace.services.common.service.ObjectPartType;
60 import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
61 import org.collectionspace.services.common.vocabulary.RefNameUtils;
62 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
63 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
64 import org.dom4j.Element;
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 import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
71
72 import org.nuxeo.ecm.core.schema.types.Schema;
73
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
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                 try{
119                     ObjectPartType partMeta = partsMetaMap.get(partLabel);
120                     // CSPACE-4030 - generates NPE if the part is missing.
121                     if(partMeta!=null) {
122                             Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
123                             if(unQObjectProperties!=null) {
124                                 addOutputPart(unQObjectProperties, partLabel, partMeta);
125                             }
126                     }
127                 } catch (Throwable t){
128
129                     logger.error("Unable to addOutputPart: "+partLabel
130                                                +" in serviceContextPath: "+this.getServiceContextPath()
131                                                +" with URI: "+this.getServiceContext().getUriInfo().getPath()
132                                                +" error: "+t);
133                 }
134                 }
135         } else {
136                 if (logger.isWarnEnabled() == true) {
137                         logger.warn("MultipartInput part was null for document id = " +
138                                         docModel.getName());
139                 }
140         }
141     }
142
143     /**
144      * Adds the output part.
145      *
146      * @param unQObjectProperties the un q object properties
147      * @param schema the schema
148      * @param partMeta the part meta
149      * @throws Exception the exception
150      * MediaType.APPLICATION_XML_TYPE
151      */
152     protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
153             throws Exception {
154         Element doc = DocumentUtils.buildDocument(partMeta, schema,
155                 unQObjectProperties);
156         if (logger.isTraceEnabled() == true) {
157             logger.trace(doc.asXML());
158         }
159         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
160         ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
161     }
162     
163     /**
164      * Extract paging info.
165      *
166      * @param commonsList the commons list
167      * @return the tL
168      * @throws Exception the exception
169      */
170     public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
171             throws Exception {
172         AbstractCommonList commonList = (AbstractCommonList) theCommonList;
173
174         DocumentFilter docFilter = this.getDocumentFilter();
175         long pageSize = docFilter.getPageSize();
176         long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
177         // set the page size and page number
178         commonList.setPageNum(pageNum);
179         commonList.setPageSize(pageSize);
180         DocumentModelList docList = wrapDoc.getWrappedObject();
181         // Set num of items in list. this is useful to our testing framework.
182         commonList.setItemsInPage(docList.size());
183         // set the total result size
184         commonList.setTotalItems(docList.totalSize());
185
186         return (TL) commonList;
187     }
188
189     /* (non-Javadoc)
190      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
191      */
192     @Override
193     public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
194             throws Exception {
195
196         DocumentModel docModel = wrapDoc.getWrappedObject();
197         String[] schemas = docModel.getDeclaredSchemas();
198         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
199         for (String schema : schemas) {
200             ObjectPartType partMeta = partsMetaMap.get(schema);
201             if (partMeta == null) {
202                 continue; // unknown part, ignore
203             }
204             Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
205             addOutputPart(unQObjectProperties, schema, partMeta);
206         }
207         addAccountPermissionsPart();
208     }
209     
210     private void addAccountPermissionsPart() throws Exception {
211         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
212         String currentServiceName = ctx.getServiceName();
213         String workflowSubResource = "/";
214         JaxRsContext jaxRsContext = ctx.getJaxRsContext();
215         if (jaxRsContext != null) {
216                 String resourceName = SecurityUtils.getResourceName(jaxRsContext.getUriInfo());
217                 workflowSubResource = workflowSubResource + resourceName + WorkflowClient.SERVICE_PATH + "/";
218         } else {
219                 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
220         }
221         AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
222                         currentServiceName, workflowSubResource);
223         org.collectionspace.services.authorization.ObjectFactory objectFactory =
224                 new org.collectionspace.services.authorization.ObjectFactory();
225         JAXBElement<AccountPermission> ap = objectFactory.createAccountPermission(accountPermission);
226         PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", ap);
227         ctx.addOutputPart(accountPermissionPart);
228     }
229
230     /* (non-Javadoc)
231      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
232      */
233     @Override
234     public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
235
236         //TODO filling extension parts should be dynamic
237         //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
238         //not an ideal way of populating objects.
239         DocumentModel docModel = wrapDoc.getWrappedObject();
240         MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
241         PoxPayloadIn input = ctx.getInput();
242         if (input.getParts().isEmpty()) {
243             String msg = "No payload found!";
244             logger.error(msg + "Ctx=" + getServiceContext().toString());
245             throw new BadRequestException(msg);
246         }
247
248         Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
249
250         //iterate over parts received and fill those parts
251         List<PayloadInputPart> inputParts = input.getParts();
252         for (PayloadInputPart part : inputParts) {
253
254             String partLabel = part.getLabel();
255             if (partLabel == null) {
256                 String msg = "Part label is missing or empty!";
257                 logger.error(msg + "Ctx=" + getServiceContext().toString());
258                 throw new BadRequestException(msg);
259             }
260
261             //skip if the part is not in metadata
262             ObjectPartType partMeta = partsMetaMap.get(partLabel);
263             if (partMeta == null) {
264                 continue;
265             }
266             fillPart(part, docModel, partMeta, action, ctx);
267         }//rof
268
269     }
270
271     /**
272      * fillPart fills an XML part into given document model
273      * @param part to fill
274      * @param docModel for the given object
275      * @param partMeta metadata for the object to fill
276      * @throws Exception
277      */
278     protected void fillPart(PayloadInputPart part, DocumentModel docModel,
279             ObjectPartType partMeta, Action action, ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx)
280             throws Exception {
281         //check if this is an xml part
282         if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
283                 Element element = part.getElementBody();
284             Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
285                 if (action == Action.UPDATE) {
286                     this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
287                 }
288                 docModel.setProperties(partMeta.getLabel(), objectProps);
289             }
290         }
291
292     /**
293      * Filters out read only properties, so they cannot be set on update.
294      * TODO: add configuration support to do this generally
295      * @param objectProps the properties parsed from the update payload
296      * @param partMeta metadata for the object to fill
297      */
298     public void filterReadOnlyPropertiesForPart(
299             Map<String, Object> objectProps, ObjectPartType partMeta) {
300         // Should add in logic to filter most of the core items on update
301         if(partMeta.getLabel().equalsIgnoreCase(COLLECTIONSPACE_CORE_SCHEMA)) {
302                 objectProps.remove(COLLECTIONSPACE_CORE_CREATED_AT);
303                 objectProps.remove(COLLECTIONSPACE_CORE_CREATED_BY);
304                 objectProps.remove(COLLECTIONSPACE_CORE_URI);
305                 objectProps.remove(COLLECTIONSPACE_CORE_TENANTID);
306                 // Note that the updatedAt/updatedBy fields are set internally
307                 // in DocumentModelHandler.handleCoreValues().
308         }
309     }
310
311     /**
312      * extractPart extracts an XML object from given DocumentModel
313      * @param docModel
314      * @param schema of the object to extract
315      * @param partMeta metadata for the object to extract
316      * @throws Exception
317      */
318     protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
319             throws Exception {
320         return extractPart(docModel, schema, (Map<String, Object>)null);
321     }
322     
323     /**
324      * extractPart extracts an XML object from given DocumentModel
325      * @param docModel
326      * @param schema of the object to extract
327      * @param partMeta metadata for the object to extract
328      * @throws Exception
329      */
330     @Deprecated
331     protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
332             throws Exception {
333         return extractPart(docModel, schema, partMeta, null);
334     }    
335
336     /**
337      * extractPart extracts an XML object from given DocumentModel
338      * @param docModel
339      * @param schema of the object to extract
340      * @param partMeta metadata for the object to extract
341      * @throws Exception
342      */
343     protected Map<String, Object> extractPart(
344             DocumentModel docModel, 
345             String schema,
346             Map<String, Object> addToMap)
347             throws Exception {
348         Map<String, Object> result = null;
349
350         Map<String, Object> objectProps = docModel.getProperties(schema);
351         if (objectProps != null) {
352                 //unqualify properties before sending the doc over the wire (to save bandwidh)
353                 //FIXME: is there a better way to avoid duplication of a Map/Collection?
354                 Map<String, Object> unQObjectProperties =
355                         (addToMap != null) ? addToMap : (new HashMap<String, Object>());
356                 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
357                 for (Entry<String, Object> entry : qualifiedEntries) {
358                     String unqProp = getUnQProperty(entry.getKey());
359                     unQObjectProperties.put(unqProp, entry.getValue());
360                 }
361                 result = unQObjectProperties;
362         }
363
364         return result;
365     }
366     
367     /**
368      * extractPart extracts an XML object from given DocumentModel
369      * @param docModel
370      * @param schema of the object to extract
371      * @param partMeta metadata for the object to extract
372      * @throws Exception
373      */
374     @Deprecated
375     protected Map<String, Object> extractPart(
376             DocumentModel docModel, String schema, ObjectPartType partMeta,
377             Map<String, Object> addToMap)
378             throws Exception {
379         Map<String, Object> result = null;
380
381         result = this.extractPart(docModel, schema, addToMap);
382
383         return result;
384     }
385     
386
387     /* (non-Javadoc)
388      * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
389      */
390     @Override
391     public AuthorityRefList getAuthorityRefs(
392             String csid,
393             List<AuthRefConfigInfo> authRefsInfo) throws PropertyException {
394
395         AuthorityRefList authRefList = new AuthorityRefList();
396         AbstractCommonList commonList = (AbstractCommonList) authRefList;
397         
398         DocumentFilter docFilter = this.getDocumentFilter();
399         long pageSize = docFilter.getPageSize();
400         long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
401         // set the page size and page number
402         commonList.setPageNum(pageNum);
403         commonList.setPageSize(pageSize);
404         
405         List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
406
407         try {
408                 int iFirstToUse = (int)(pageSize*pageNum);
409                 int nFoundInPage = 0;
410                 int nFoundTotal = 0;
411                 
412                 ArrayList<RefNameServiceUtils.AuthRefInfo> foundProps 
413                         = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
414                 
415                 boolean releaseRepoSession = false;
416                 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
417                 RepositoryJavaClientImpl repoClient = (RepositoryJavaClientImpl)this.getRepositoryClient(ctx);
418                 RepositoryInstance repoSession = this.getRepositorySession();
419                 if (repoSession == null) {
420                         repoSession = repoClient.getRepositorySession();
421                         releaseRepoSession = true;
422                 }
423                 
424                 try {
425                         DocumentModel docModel = repoClient.getDoc(repoSession, ctx, csid).getWrappedObject();
426                         RefNameServiceUtils.findAuthRefPropertiesInDoc(docModel, authRefsInfo, null, foundProps);
427                         // Slightly goofy pagination support - how many refs do we expect from one object?
428                         for(RefNameServiceUtils.AuthRefInfo ari:foundProps) {
429                                 if((nFoundTotal >= iFirstToUse) && (nFoundInPage < pageSize)) {
430                                         if(appendToAuthRefsList(ari, list)) {
431                                                 nFoundInPage++;
432                                         nFoundTotal++;
433                                         }
434                                 } else {
435                                         nFoundTotal++;
436                                 }
437                         }
438                 } finally {
439                         if (releaseRepoSession == true) {
440                                 repoClient.releaseRepositorySession(repoSession);
441                         }
442                 }
443                 
444             // Set num of items in list. this is useful to our testing framework.
445             commonList.setItemsInPage(nFoundInPage);
446             // set the total result size
447             commonList.setTotalItems(nFoundTotal);
448             
449         } catch (PropertyException pe) {
450             String msg = "Attempted to retrieve value for invalid or missing authority field. "
451                     + "Check authority field properties in tenant bindings.";
452             logger.warn(msg, pe);
453             throw pe;
454         } catch (Exception e) {
455             if (logger.isDebugEnabled()) {
456                 logger.debug("Caught exception in getAuthorityRefs", e);
457             }
458             Response response = Response.status(
459                     Response.Status.INTERNAL_SERVER_ERROR).entity(
460                     "Failed to retrieve authority references").type(
461                     "text/plain").build();
462             throw new WebApplicationException(response);
463         }
464
465         return authRefList;
466     }
467
468     private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari, 
469                                                 List<AuthorityRefList.AuthorityRefItem> list)
470             throws Exception {
471         String fieldName = ari.getQualifiedDisplayName();
472         try {
473                         String refNameValue = (String)ari.getProperty().getValue();
474                         AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
475                         if(item!=null) {        // ignore garbage values.
476                                 list.add(item);
477                                 return true;
478                         }
479         } catch(PropertyException pe) {
480                         logger.debug("PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage());
481         }
482         return false;
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                 logger.error("Trouble parsing refName from value: "+refName+" in field: "+authRefFieldName+e.getLocalizedMessage());
497                 ilistItem = null;
498         }
499         return ilistItem;
500     }
501
502     /**
503      * Returns the primary value from a list of values.
504      *
505      * Assumes that the first value is the primary value.
506      * This assumption may change when and if the primary value
507      * is identified explicitly.
508      *
509      * @param values a list of values.
510      * @param propertyName the name of a property through
511      *     which the value can be extracted.
512      * @return the primary value.
513     protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
514         String primaryValue = "";
515         if (values == null || values.size() == 0) {
516             return primaryValue;
517         }
518         Object value = values.get(0);
519         if (value instanceof String) {
520             if (value != null) {
521                 primaryValue = (String) value;
522             }
523        // Multivalue group of fields
524        } else if (value instanceof Map) {
525             if (value != null) {
526                 Map map = (Map) value;
527                 if (map.values().size() > 0) {
528                     if (map.get(propertyName) != null) {
529                       primaryValue = (String) map.get(propertyName);
530                     }
531                 }
532             }
533        } else {
534             logger.warn("Unexpected type for property " + propertyName
535                     + " in multivalue list: not String or Map.");
536        }
537        return primaryValue;
538     }
539      */
540
541     /**
542      * Gets a simple property from the document.
543      *
544      * For completeness, as this duplicates DocumentModel method. 
545      *
546      * @param docModel The document model to get info from
547      * @param schema The name of the schema (part)
548      * @param propertyName The simple scalar property type
549      * @return property value as String
550      */
551     protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
552         String xpath = "/"+schema+":"+propName;
553         try {
554                 return (String)docModel.getPropertyValue(xpath);
555         } catch(PropertyException pe) {
556                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
557                                 +pe.getLocalizedMessage());
558         } catch(ClassCastException cce) {
559                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar 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      * 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 getFirstRepeatingStringProperty(
576                 DocumentModel docModel, String schema, String listName) {
577         String xpath = "/"+schema+":"+listName+"/[0]";
578         try {
579                 return (String)docModel.getPropertyValue(xpath);
580         } catch(PropertyException pe) {
581                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a repeating scalar?"
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 repeating 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     /**
597      * Gets first of a repeating list of scalar values, as a String, from the document.
598      *
599      * @param docModel The document model to get info from
600      * @param schema The name of the schema (part)
601      * @param listName The name of the scalar list property
602      * @return first value in list, as a String, or empty string if the list is empty
603      */
604     protected String getStringValueInPrimaryRepeatingComplexProperty(
605                 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
606         String xpath = "/"+schema+":"+complexPropertyName+"/[0]/"+fieldName;
607         try {
608                 return (String)docModel.getPropertyValue(xpath);
609         } catch(PropertyException pe) {
610                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
611                                 +pe.getLocalizedMessage());
612         } catch(IndexOutOfBoundsException ioobe) {
613                 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
614                 return "";      // gracefully handle missing elements
615         } catch(ClassCastException cce) {
616                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
617                                 +cce.getLocalizedMessage());
618         } catch(Exception e) {
619                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
620                                 +e.getLocalizedMessage());
621         }
622     }
623    
624     /**
625      * Gets XPath value from schema. Note that only "/" and "[n]" are
626      * supported for xpath. Can omit grouping elements for repeating complex types, 
627      * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
628      * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
629      * If there are no entries for a list of scalars or for a list of complex types, 
630      * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
631      * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
632      * that many elements in the list. 
633      * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
634      *
635      * @param docModel The document model to get info from
636      * @param schema The name of the schema (part)
637      * @param xpath The XPath expression (without schema prefix)
638      * @return value the indicated property value as a String
639      */
640     protected static String getXPathStringValue(DocumentModel docModel, String schema, String xpath) {
641         xpath = schema+":"+xpath;
642         try {
643                 Object value = docModel.getPropertyValue(xpath);
644                 String returnVal = null;
645                 if(value==null) {
646                         // Nothing to do - leave returnVal null
647                 } else if(value instanceof GregorianCalendar) {
648                         returnVal = DateTimeFormatUtils.formatAsISO8601Timestamp((GregorianCalendar)value);
649                 } else {
650                         returnVal = value.toString();
651                 }
652                 return returnVal;
653         } catch(PropertyException pe) {
654                 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad XPath spec?"
655                                 +pe.getLocalizedMessage());
656         } catch(ClassCastException cce) {
657                 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
658                                 +cce.getLocalizedMessage());
659         } catch(IndexOutOfBoundsException ioobe) {
660                 // Nuxeo seems to handle foo/[0]/bar when it is missing,
661                 // but not foo/bar[0] (for repeating scalars).
662                 if(xpath.endsWith("[0]")) {             // gracefully handle missing elements
663                         return "";
664                 } else {
665                         String msg = ioobe.getMessage();
666                         if(msg!=null && msg.equals("Index: 0, Size: 0")) {
667                                 // Some other variant on a missing sub-field; quietly absorb.
668                                 return "";
669                         } // Otherwise, e.g., for true OOB indices, propagate the exception.
670                 }
671                 throw new RuntimeException("Problem retrieving property {"+xpath+"}:"
672                                 +ioobe.getLocalizedMessage());
673         } catch(Exception e) {
674                 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
675                                 +e.getLocalizedMessage());
676         }
677     }
678    
679 }