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