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