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.GregorianCalendar;
28 import java.util.HashMap;
29 import java.util.List;
31 import java.util.Map.Entry;
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;
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;
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;
70 import org.nuxeo.ecm.core.schema.types.Schema;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
76 * RemoteDocumentModelHandler
78 * $LastChangedRevision: $
83 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
84 extends DocumentModelHandler<T, TL> {
87 private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
90 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
93 public void setServiceContext(ServiceContext ctx) { //FIXME: Apply proper generics to ServiceContext<PoxPayloadIn, PoxPayloadOut>
94 if (ctx instanceof MultipartServiceContext) {
95 super.setServiceContext(ctx);
97 throw new IllegalArgumentException("setServiceContext requires instance of "
98 + MultipartServiceContext.class.getName());
103 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
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();
113 List<PayloadInputPart> inputParts = ctx.getInput().getParts();
114 for (PayloadInputPart part : inputParts) {
115 String partLabel = part.getLabel();
117 ObjectPartType partMeta = partsMetaMap.get(partLabel);
118 // CSPACE-4030 - generates NPE if the part is missing.
120 Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
121 if(unQObjectProperties!=null) {
122 addOutputPart(unQObjectProperties, partLabel, partMeta);
125 } catch (Throwable t){
127 logger.error("Unable to addOutputPart: "+partLabel
128 +" in serviceContextPath: "+this.getServiceContextPath()
129 +" with URI: "+this.getServiceContext().getUriInfo().getPath()
134 if (logger.isWarnEnabled() == true) {
135 logger.warn("MultipartInput part was null for document id = " +
142 * Adds the output part.
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
150 protected void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
152 Element doc = DocumentUtils.buildDocument(partMeta, schema,
153 unQObjectProperties);
154 if (logger.isTraceEnabled() == true) {
155 logger.trace(doc.asXML());
157 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
158 ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
162 * Extract paging info.
164 * @param commonsList the commons list
166 * @throws Exception the exception
168 public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
170 AbstractCommonList commonList = (AbstractCommonList) theCommonList;
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());
184 return (TL) commonList;
188 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
191 public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
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
202 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
203 if(COLLECTIONSPACE_CORE_SCHEMA.equals(schema)) {
204 addExtraCoreValues(docModel, unQObjectProperties);
206 addOutputPart(unQObjectProperties, schema, partMeta);
208 addAccountPermissionsPart();
211 private void addExtraCoreValues(DocumentModel docModel, Map<String, Object> unQObjectProperties)
213 unQObjectProperties.put(COLLECTIONSPACE_CORE_WORKFLOWSTATE, docModel.getCurrentLifeCycleState());
216 private void addAccountPermissionsPart() throws Exception {
217 Profiler profiler = new Profiler("addAccountPermissionsPart():", 1);
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 + "/";
228 workflowSubResource = workflowSubResource + currentServiceName + WorkflowClient.SERVICE_AUTHZ_SUFFIX;
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);
242 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
245 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
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);
259 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
261 //iterate over parts received and fill those parts
262 List<PayloadInputPart> inputParts = input.getParts();
263 for (PayloadInputPart part : inputParts) {
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);
272 //skip if the part is not in metadata
273 ObjectPartType partMeta = partsMetaMap.get(partLabel);
274 if (partMeta == null) {
277 fillPart(part, docModel, partMeta, action, ctx);
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
289 protected void fillPart(PayloadInputPart part, DocumentModel docModel,
290 ObjectPartType partMeta, Action action, ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx)
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);
299 docModel.setProperties(partMeta.getLabel(), objectProps);
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
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().
323 * extractPart extracts an XML object from given DocumentModel
325 * @param schema of the object to extract
326 * @param partMeta metadata for the object to extract
329 protected Map<String, Object> extractPart(DocumentModel docModel, String schema)
331 return extractPart(docModel, schema, (Map<String, Object>)null);
335 * extractPart extracts an XML object from given DocumentModel
337 * @param schema of the object to extract
338 * @param partMeta metadata for the object to extract
342 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
344 return extractPart(docModel, schema, partMeta, null);
348 * extractPart extracts an XML object from given DocumentModel
350 * @param schema of the object to extract
351 * @param partMeta metadata for the object to extract
354 protected Map<String, Object> extractPart(
355 DocumentModel docModel,
357 Map<String, Object> addToMap)
359 Map<String, Object> result = null;
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());
372 result = unQObjectProperties;
379 * extractPart extracts an XML object from given DocumentModel
381 * @param schema of the object to extract
382 * @param partMeta metadata for the object to extract
386 protected Map<String, Object> extractPart(
387 DocumentModel docModel, String schema, ObjectPartType partMeta,
388 Map<String, Object> addToMap)
390 Map<String, Object> result = null;
392 result = this.extractPart(docModel, schema, addToMap);
398 public String getStringPropertyFromDoc(
401 String propertyXPath ) throws DocumentNotFoundException, DocumentException {
402 RepositoryInstance repoSession = null;
403 boolean releaseRepoSession = false;
404 String returnValue = null;
407 RepositoryJavaClientImpl repoClient = (RepositoryJavaClientImpl)this.getRepositoryClient(ctx);
408 repoSession = this.getRepositorySession();
409 if (repoSession == null) {
410 repoSession = repoClient.getRepositorySession();
411 releaseRepoSession = true;
415 DocumentWrapper<DocumentModel> wrapper = repoClient.getDoc(repoSession, ctx, csid);
416 DocumentModel docModel = wrapper.getWrappedObject();
417 returnValue = (String) docModel.getPropertyValue(propertyXPath);
418 } catch (PropertyException pe) {
420 } catch (DocumentException de) {
422 } catch (Exception e) {
423 if (logger.isDebugEnabled()) {
424 logger.debug("Caught exception ", e);
426 throw new DocumentException(e);
428 if (releaseRepoSession && repoSession != null) {
429 repoClient.releaseRepositorySession(repoSession);
432 } catch (Exception e) {
433 if (logger.isDebugEnabled()) {
434 logger.debug("Caught exception ", e);
436 throw new DocumentException(e);
440 if (logger.isWarnEnabled() == true) {
441 logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
450 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
453 public AuthorityRefList getAuthorityRefs(
455 List<AuthRefConfigInfo> authRefsInfo) throws PropertyException {
457 AuthorityRefList authRefList = new AuthorityRefList();
458 AbstractCommonList commonList = (AbstractCommonList) authRefList;
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);
467 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
470 int iFirstToUse = (int)(pageSize*pageNum);
471 int nFoundInPage = 0;
474 ArrayList<RefNameServiceUtils.AuthRefInfo> foundProps
475 = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
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;
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)) {
501 if (releaseRepoSession == true) {
502 repoClient.releaseRepositorySession(repoSession);
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);
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);
516 } catch (Exception e) {
517 if (logger.isDebugEnabled()) {
518 logger.debug("Caught exception in getAuthorityRefs", e);
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);
530 private boolean appendToAuthRefsList(RefNameServiceUtils.AuthRefInfo ari,
531 List<AuthorityRefList.AuthorityRefItem> list)
533 String fieldName = ari.getQualifiedDisplayName();
535 String refNameValue = (String)ari.getProperty().getValue();
536 AuthorityRefList.AuthorityRefItem item = authorityRefListItem(fieldName, refNameValue);
537 if(item!=null) { // ignore garbage values.
541 } catch(PropertyException pe) {
542 logger.debug("PropertyException on: "+ari.getProperty().getPath()+pe.getLocalizedMessage());
547 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
549 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
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());
565 * Returns the primary value from a list of values.
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.
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) {
580 Object value = values.get(0);
581 if (value instanceof String) {
583 primaryValue = (String) value;
585 // Multivalue group of fields
586 } else if (value instanceof Map) {
588 Map map = (Map) value;
589 if (map.values().size() > 0) {
590 if (map.get(propertyName) != null) {
591 primaryValue = (String) map.get(propertyName);
596 logger.warn("Unexpected type for property " + propertyName
597 + " in multivalue list: not String or Map.");
604 * Gets a simple property from the document.
606 * For completeness, as this duplicates DocumentModel method.
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
613 protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
614 String xpath = "/"+schema+":"+propName;
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());
630 * Gets first of a repeating list of scalar values, as a String, from the document.
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
637 protected String getFirstRepeatingStringProperty(
638 DocumentModel docModel, String schema, String listName) {
639 String xpath = "/"+schema+":"+listName+"/[0]";
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());
659 * Gets first of a repeating list of scalar values, as a String, from the document.
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
666 protected String getStringValueInPrimaryRepeatingComplexProperty(
667 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
668 String xpath = "/"+schema+":"+complexPropertyName+"/[0]/"+fieldName;
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());
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.
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
702 protected static String getXPathStringValue(DocumentModel docModel, String schema, String xpath) {
703 xpath = schema+":"+xpath;
705 Object value = docModel.getPropertyValue(xpath);
706 String returnVal = null;
708 // Nothing to do - leave returnVal null
709 } else if(value instanceof GregorianCalendar) {
710 returnVal = DateTimeFormatUtils.formatAsISO8601Timestamp((GregorianCalendar)value);
712 returnVal = value.toString();
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
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.
731 } // Otherwise, e.g., for true OOB indices, propagate the exception.
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());