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