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:
6 * http://www.collectionspace.org
7 * http://wiki.collectionspace.org
9 * Copyright 2009 University of California at Berkeley
11 * Licensed under the Educational Community License (ECL), Version 2.0.
12 * You may not use this file except in compliance with this License.
14 * You may obtain a copy of the ECL 2.0 License at
16 * https://source.collectionspace.org/collection-space/LICENSE.txt
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.
24 package org.collectionspace.services.nuxeo.client.java;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.GregorianCalendar;
29 import java.util.HashMap;
30 import java.util.List;
32 import java.util.Map.Entry;
35 import javax.ws.rs.WebApplicationException;
36 import javax.ws.rs.core.MediaType;
37 import javax.ws.rs.core.Response;
38 import javax.xml.bind.JAXBElement;
40 import org.collectionspace.services.authorization.AccountPermission;
41 import org.collectionspace.services.jaxb.AbstractCommonList;
42 import org.collectionspace.services.client.PayloadInputPart;
43 import org.collectionspace.services.client.PayloadOutputPart;
44 import org.collectionspace.services.client.PoxPayloadIn;
45 import org.collectionspace.services.client.PoxPayloadOut;
46 import org.collectionspace.services.client.workflow.WorkflowClient;
47 import org.collectionspace.services.common.api.Tools;
48 import org.collectionspace.services.common.authorityref.AuthorityRefList;
49 import org.collectionspace.services.common.context.JaxRsContext;
50 import org.collectionspace.services.common.context.MultipartServiceContext;
51 import org.collectionspace.services.common.context.ServiceContext;
52 import org.collectionspace.services.common.datetime.DateTimeFormatUtils;
53 import org.collectionspace.services.common.document.BadRequestException;
54 import org.collectionspace.services.common.document.DocumentException;
55 import org.collectionspace.services.common.document.DocumentNotFoundException;
56 import org.collectionspace.services.common.document.DocumentUtils;
57 import org.collectionspace.services.common.document.DocumentWrapper;
58 import org.collectionspace.services.common.document.DocumentFilter;
59 import org.collectionspace.services.common.document.DocumentHandler.Action;
60 import org.collectionspace.services.common.security.SecurityUtils;
61 import org.collectionspace.services.common.service.ObjectPartType;
62 import org.collectionspace.services.common.storage.jpa.JpaStorageUtils;
63 import org.collectionspace.services.common.vocabulary.RefNameUtils;
64 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
65 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
66 import org.dom4j.Element;
68 import org.nuxeo.ecm.core.api.DocumentModel;
69 import org.nuxeo.ecm.core.api.DocumentModelList;
70 import org.nuxeo.ecm.core.api.model.Property;
71 import org.nuxeo.ecm.core.api.model.PropertyException;
72 import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
74 import org.nuxeo.ecm.core.schema.types.Schema;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
80 * RemoteDocumentModelHandler
82 * $LastChangedRevision: $
87 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
88 extends DocumentModelHandler<T, TL> {
91 private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
94 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
97 public void setServiceContext(ServiceContext ctx) { //FIXME: Apply proper generics to ServiceContext<PoxPayloadIn, PoxPayloadOut>
98 if (ctx instanceof MultipartServiceContext) {
99 super.setServiceContext(ctx);
101 throw new IllegalArgumentException("setServiceContext requires instance of "
102 + MultipartServiceContext.class.getName());
107 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
110 public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
111 DocumentModel docModel = wrapDoc.getWrappedObject();
112 //return at least those document part(s) that were received
113 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
114 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
115 PoxPayloadIn input = ctx.getInput();
117 List<PayloadInputPart> inputParts = ctx.getInput().getParts();
118 for (PayloadInputPart part : inputParts) {
119 String partLabel = part.getLabel();
121 ObjectPartType partMeta = partsMetaMap.get(partLabel);
122 // CSPACE-4030 - generates NPE if the part is missing.
124 Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
125 if(unQObjectProperties!=null) {
126 addOutputPart(unQObjectProperties, partLabel, partMeta);
129 } catch (Throwable t){
131 logger.error("Unable to addOutputPart: "+partLabel
132 +" in serviceContextPath: "+this.getServiceContextPath()
133 +" with URI: "+this.getServiceContext().getUriInfo().getPath()
138 if (logger.isWarnEnabled() == true) {
139 logger.warn("MultipartInput part was null for document id = " +
146 * Adds the output part.
148 * @param unQObjectProperties the un q object properties
149 * @param schema the schema
150 * @param partMeta the part meta
151 * @throws Exception the exception
152 * MediaType.APPLICATION_XML_TYPE
154 protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
156 Element doc = DocumentUtils.buildDocument(partMeta, schema,
157 unQObjectProperties);
158 if (logger.isTraceEnabled() == true) {
159 logger.trace(doc.asXML());
161 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
162 ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
166 * Extract paging info.
168 * @param commonsList the commons list
170 * @throws Exception the exception
172 public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
174 AbstractCommonList commonList = (AbstractCommonList) theCommonList;
176 DocumentFilter docFilter = this.getDocumentFilter();
177 long pageSize = docFilter.getPageSize();
178 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
179 // set the page size and page number
180 commonList.setPageNum(pageNum);
181 commonList.setPageSize(pageSize);
182 DocumentModelList docList = wrapDoc.getWrappedObject();
183 // Set num of items in list. this is useful to our testing framework.
184 commonList.setItemsInPage(docList.size());
185 // set the total result size
186 commonList.setTotalItems(docList.totalSize());
188 return (TL) commonList;
192 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
195 public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
198 DocumentModel docModel = wrapDoc.getWrappedObject();
199 String[] schemas = docModel.getDeclaredSchemas();
200 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
201 for (String schema : schemas) {
202 ObjectPartType partMeta = partsMetaMap.get(schema);
203 if (partMeta == null) {
204 continue; // unknown part, ignore
206 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
207 addOutputPart(unQObjectProperties, schema, partMeta);
209 addAccountPermissionsPart();
212 private void addAccountPermissionsPart() throws Exception {
213 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
214 String currentServiceName = ctx.getServiceName();
215 String workflowSubResource = "/";
216 JaxRsContext jaxRsContext = ctx.getJaxRsContext();
217 if (jaxRsContext != null) {
218 String resourceName = SecurityUtils.getResourceName(jaxRsContext.getUriInfo());
219 workflowSubResource = workflowSubResource + resourceName + WorkflowClient.SERVICE_PATH + "/";
221 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
223 AccountPermission accountPermission = JpaStorageUtils.getAccountPermissions(JpaStorageUtils.CS_CURRENT_USER,
224 currentServiceName, workflowSubResource);
225 org.collectionspace.services.authorization.ObjectFactory objectFactory =
226 new org.collectionspace.services.authorization.ObjectFactory();
227 JAXBElement<AccountPermission> ap = objectFactory.createAccountPermission(accountPermission);
228 PayloadOutputPart accountPermissionPart = new PayloadOutputPart("account_permission", ap);
229 ctx.addOutputPart(accountPermissionPart);
233 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
236 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
238 //TODO filling extension parts should be dynamic
239 //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
240 //not an ideal way of populating objects.
241 DocumentModel docModel = wrapDoc.getWrappedObject();
242 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
243 PoxPayloadIn input = ctx.getInput();
244 if (input.getParts().isEmpty()) {
245 String msg = "No payload found!";
246 logger.error(msg + "Ctx=" + getServiceContext().toString());
247 throw new BadRequestException(msg);
250 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
252 //iterate over parts received and fill those parts
253 List<PayloadInputPart> inputParts = input.getParts();
254 for (PayloadInputPart part : inputParts) {
256 String partLabel = part.getLabel();
257 if (partLabel == null) {
258 String msg = "Part label is missing or empty!";
259 logger.error(msg + "Ctx=" + getServiceContext().toString());
260 throw new BadRequestException(msg);
263 //skip if the part is not in metadata
264 ObjectPartType partMeta = partsMetaMap.get(partLabel);
265 if (partMeta == null) {
268 fillPart(part, docModel, partMeta, action, ctx);
274 * fillPart fills an XML part into given document model
275 * @param part to fill
276 * @param docModel for the given object
277 * @param partMeta metadata for the object to fill
280 protected void fillPart(PayloadInputPart part, DocumentModel docModel,
281 ObjectPartType partMeta, Action action, ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx)
283 //check if this is an xml part
284 if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
285 Element element = part.getElementBody();
286 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, element, ctx);
287 if (action == Action.UPDATE) {
288 this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
290 docModel.setProperties(partMeta.getLabel(), objectProps);
295 * Filters out read only properties, so they cannot be set on update.
296 * TODO: add configuration support to do this generally
297 * @param objectProps the properties parsed from the update payload
298 * @param partMeta metadata for the object to fill
300 public void filterReadOnlyPropertiesForPart(
301 Map<String, Object> objectProps, ObjectPartType partMeta) {
302 // Should add in logic to filter most of the core items on update
303 if(partMeta.getLabel().equalsIgnoreCase(COLLECTIONSPACE_CORE_SCHEMA)) {
304 objectProps.remove(COLLECTIONSPACE_CORE_CREATED_AT);
305 objectProps.remove(COLLECTIONSPACE_CORE_CREATED_BY);
306 objectProps.remove(COLLECTIONSPACE_CORE_URI);
307 objectProps.remove(COLLECTIONSPACE_CORE_TENANTID);
308 // Note that the updatedAt/updatedBy fields are set internally
309 // in DocumentModelHandler.handleCoreValues().
314 * extractPart extracts an XML object from given DocumentModel
316 * @param schema of the object to extract
317 * @param partMeta metadata for the object to extract
320 protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
322 return extractPart(docModel, schema, (Map<String, Object>)null);
326 * extractPart extracts an XML object from given DocumentModel
328 * @param schema of the object to extract
329 * @param partMeta metadata for the object to extract
333 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
335 return extractPart(docModel, schema, partMeta, null);
339 * extractPart extracts an XML object from given DocumentModel
341 * @param schema of the object to extract
342 * @param partMeta metadata for the object to extract
345 protected Map<String, Object> extractPart(
346 DocumentModel docModel,
348 Map<String, Object> addToMap)
350 Map<String, Object> result = null;
352 Map<String, Object> objectProps = docModel.getProperties(schema);
353 if (objectProps != null) {
354 //unqualify properties before sending the doc over the wire (to save bandwidh)
355 //FIXME: is there a better way to avoid duplication of a Map/Collection?
356 Map<String, Object> unQObjectProperties =
357 (addToMap != null) ? addToMap : (new HashMap<String, Object>());
358 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
359 for (Entry<String, Object> entry : qualifiedEntries) {
360 String unqProp = getUnQProperty(entry.getKey());
361 unQObjectProperties.put(unqProp, entry.getValue());
363 result = unQObjectProperties;
370 * extractPart extracts an XML object from given DocumentModel
372 * @param schema of the object to extract
373 * @param partMeta metadata for the object to extract
377 protected Map<String, Object> extractPart(
378 DocumentModel docModel, String schema, ObjectPartType partMeta,
379 Map<String, Object> addToMap)
381 Map<String, Object> result = null;
383 result = this.extractPart(docModel, schema, addToMap);
389 public String getStringPropertyFromDoc(
392 String propertyXPath ) throws DocumentNotFoundException, DocumentException {
393 RepositoryInstance repoSession = null;
394 boolean releaseRepoSession = false;
395 String returnValue = null;
398 RepositoryJavaClientImpl repoClient = (RepositoryJavaClientImpl)this.getRepositoryClient(ctx);
399 repoSession = this.getRepositorySession();
400 if (repoSession == null) {
401 repoSession = repoClient.getRepositorySession();
402 releaseRepoSession = true;
406 DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, csid);
407 DocumentModel docModel = wrapper.getWrappedObject();
408 returnValue = (String) docModel.getPropertyValue(propertyXPath);
409 } catch (PropertyException pe) {
411 } catch (DocumentException de) {
413 } catch (Exception e) {
414 if (logger.isDebugEnabled()) {
415 logger.debug("Caught exception ", e);
417 throw new DocumentException(e);
419 if (releaseRepoSession && repoSession != null) {
420 repoClient.releaseRepositorySession(repoSession);
423 } catch (Exception e) {
424 if (logger.isDebugEnabled()) {
425 logger.debug("Caught exception ", e);
427 throw new DocumentException(e);
431 if (logger.isWarnEnabled() == true) {
432 logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
441 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
444 public AuthorityRefList getAuthorityRefs(
446 List<AuthRefConfigInfo> authRefsInfo) throws PropertyException {
448 AuthorityRefList authRefList = new AuthorityRefList();
449 AbstractCommonList commonList = (AbstractCommonList) authRefList;
451 DocumentFilter docFilter = this.getDocumentFilter();
452 long pageSize = docFilter.getPageSize();
453 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
454 // set the page size and page number
455 commonList.setPageNum(pageNum);
456 commonList.setPageSize(pageSize);
458 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
461 int iFirstToUse = (int)(pageSize*pageNum);
462 int nFoundInPage = 0;
465 ArrayList<RefNameServiceUtils.AuthRefInfo> foundProps
466 = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
468 boolean releaseRepoSession = false;
469 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
470 RepositoryJavaClientImpl repoClient = (RepositoryJavaClientImpl)this.getRepositoryClient(ctx);
471 RepositoryInstance repoSession = this.getRepositorySession();
472 if (repoSession == null) {
473 repoSession = repoClient.getRepositorySession();
474 releaseRepoSession = true;
478 DocumentModel docModel = repoClient.getDoc(repoSession, ctx, csid).getWrappedObject();
479 RefNameServiceUtils.findAuthRefPropertiesInDoc(docModel, authRefsInfo, null, foundProps);
480 // Slightly goofy pagination support - how many refs do we expect from one object?
481 for(RefNameServiceUtils.AuthRefInfo ari:foundProps) {
482 if((nFoundTotal >= iFirstToUse) && (nFoundInPage < pageSize)) {
483 if(appendToAuthRefsList(ari, list)) {
492 if (releaseRepoSession == true) {
493 repoClient.releaseRepositorySession(repoSession);
497 // Set num of items in list. this is useful to our testing framework.
498 commonList.setItemsInPage(nFoundInPage);
499 // set the total result size
500 commonList.setTotalItems(nFoundTotal);
502 } catch (PropertyException pe) {
503 String msg = "Attempted to retrieve value for invalid or missing authority field. "
504 + "Check authority field properties in tenant bindings.";
505 logger.warn(msg, pe);
507 } catch (Exception e) {
508 if (logger.isDebugEnabled()) {
509 logger.debug("Caught exception in getAuthorityRefs", e);
511 Response response = Response.status(
512 Response.Status.INTERNAL_SERVER_ERROR).entity(
513 "Failed to retrieve authority references").type(
514 "text/plain").build();
515 throw new WebApplicationException(response);
521 private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari,
522 List<AuthorityRefList.AuthorityRefItem> list)
524 String fieldName = ari.getQualifiedDisplayName();
526 String refNameValue = (String)ari.getProperty().getValue();
527 AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
528 if(item!=null) { // ignore garbage values.
532 } catch(PropertyException pe) {
533 logger.debug("PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage());
538 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
540 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
542 RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
543 ilistItem.setRefName(refName);
544 ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
545 ilistItem.setItemDisplayName(termInfo.displayName);
546 ilistItem.setSourceField(authRefFieldName);
547 ilistItem.setUri(termInfo.getRelativeUri());
548 } catch (Exception e) {
549 logger.error("Trouble parsing refName from value: "+refName+" in field: "+authRefFieldName+e.getLocalizedMessage());
556 * Returns the primary value from a list of values.
558 * Assumes that the first value is the primary value.
559 * This assumption may change when and if the primary value
560 * is identified explicitly.
562 * @param values a list of values.
563 * @param propertyName the name of a property through
564 * which the value can be extracted.
565 * @return the primary value.
566 protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
567 String primaryValue = "";
568 if (values == null || values.size() == 0) {
571 Object value = values.get(0);
572 if (value instanceof String) {
574 primaryValue = (String) value;
576 // Multivalue group of fields
577 } else if (value instanceof Map) {
579 Map map = (Map) value;
580 if (map.values().size() > 0) {
581 if (map.get(propertyName) != null) {
582 primaryValue = (String) map.get(propertyName);
587 logger.warn("Unexpected type for property " + propertyName
588 + " in multivalue list: not String or Map.");
595 * Gets a simple property from the document.
597 * For completeness, as this duplicates DocumentModel method.
599 * @param docModel The document model to get info from
600 * @param schema The name of the schema (part)
601 * @param propertyName The simple scalar property type
602 * @return property value as String
604 protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
605 String xpath = "/"+schema+":"+propName;
607 return (String)docModel.getPropertyValue(xpath);
608 } catch(PropertyException pe) {
609 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
610 +pe.getLocalizedMessage());
611 } catch(ClassCastException cce) {
612 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
613 +cce.getLocalizedMessage());
614 } catch(Exception e) {
615 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
616 +e.getLocalizedMessage());
621 * Gets first of a repeating list of scalar values, as a String, from the document.
623 * @param docModel The document model to get info from
624 * @param schema The name of the schema (part)
625 * @param listName The name of the scalar list property
626 * @return first value in list, as a String, or empty string if the list is empty
628 protected String getFirstRepeatingStringProperty(
629 DocumentModel docModel, String schema, String listName) {
630 String xpath = "/"+schema+":"+listName+"/[0]";
632 return (String)docModel.getPropertyValue(xpath);
633 } catch(PropertyException pe) {
634 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a repeating scalar?"
635 +pe.getLocalizedMessage());
636 } catch(IndexOutOfBoundsException ioobe) {
637 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
638 return ""; // gracefully handle missing elements
639 } catch(ClassCastException cce) {
640 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a repeating String property?"
641 +cce.getLocalizedMessage());
642 } catch(Exception e) {
643 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
644 +e.getLocalizedMessage());
650 * Gets first of a repeating list of scalar values, as a String, from the document.
652 * @param docModel The document model to get info from
653 * @param schema The name of the schema (part)
654 * @param listName The name of the scalar list property
655 * @return first value in list, as a String, or empty string if the list is empty
657 protected String getStringValueInPrimaryRepeatingComplexProperty(
658 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
659 String xpath = "/"+schema+":"+complexPropertyName+"/[0]/"+fieldName;
661 return (String)docModel.getPropertyValue(xpath);
662 } catch(PropertyException pe) {
663 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
664 +pe.getLocalizedMessage());
665 } catch(IndexOutOfBoundsException ioobe) {
666 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
667 return ""; // gracefully handle missing elements
668 } catch(ClassCastException cce) {
669 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
670 +cce.getLocalizedMessage());
671 } catch(Exception e) {
672 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
673 +e.getLocalizedMessage());
678 * Gets XPath value from schema. Note that only "/" and "[n]" are
679 * supported for xpath. Can omit grouping elements for repeating complex types,
680 * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
681 * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
682 * If there are no entries for a list of scalars or for a list of complex types,
683 * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
684 * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
685 * that many elements in the list.
686 * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
688 * @param docModel The document model to get info from
689 * @param schema The name of the schema (part)
690 * @param xpath The XPath expression (without schema prefix)
691 * @return value the indicated property value as a String
693 protected static String getXPathStringValue(DocumentModel docModel, String schema, String xpath) {
694 xpath = schema+":"+xpath;
696 Object value = docModel.getPropertyValue(xpath);
697 String returnVal = null;
699 // Nothing to do - leave returnVal null
700 } else if(value instanceof GregorianCalendar) {
701 returnVal = DateTimeFormatUtils.formatAsISO8601Timestamp((GregorianCalendar)value);
703 returnVal = value.toString();
706 } catch(PropertyException pe) {
707 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad XPath spec?"
708 +pe.getLocalizedMessage());
709 } catch(ClassCastException cce) {
710 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
711 +cce.getLocalizedMessage());
712 } catch(IndexOutOfBoundsException ioobe) {
713 // Nuxeo seems to handle foo/[0]/bar when it is missing,
714 // but not foo/bar[0] (for repeating scalars).
715 if(xpath.endsWith("[0]")) { // gracefully handle missing elements
718 String msg = ioobe.getMessage();
719 if(msg!=null && msg.equals("Index: 0, Size: 0")) {
720 // Some other variant on a missing sub-field; quietly absorb.
722 } // Otherwise, e.g., for true OOB indices, propagate the exception.
724 throw new RuntimeException("Problem retrieving property {"+xpath+"}:"
725 +ioobe.getLocalizedMessage());
726 } catch(Exception e) {
727 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
728 +e.getLocalizedMessage());