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