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.io.InputStream;
27 import java.util.Collection;
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;
38 import org.collectionspace.services.jaxb.AbstractCommonList;
39 import org.collectionspace.services.common.authorityref.AuthorityRefList;
40 import org.collectionspace.services.common.context.MultipartServiceContext;
41 import org.collectionspace.services.common.context.ServiceContext;
42 import org.collectionspace.services.common.document.BadRequestException;
43 import org.collectionspace.services.common.document.DocumentUtils;
44 import org.collectionspace.services.common.document.DocumentWrapper;
45 import org.collectionspace.services.common.document.DocumentFilter;
46 import org.collectionspace.services.common.document.DocumentHandler.Action;
47 import org.collectionspace.services.common.service.ObjectPartType;
48 import org.collectionspace.services.common.vocabulary.RefNameUtils;
50 import org.jboss.resteasy.plugins.providers.multipart.InputPart;
51 import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
53 import org.nuxeo.ecm.core.api.DocumentModel;
54 import org.nuxeo.ecm.core.api.DocumentModelList;
55 import org.nuxeo.ecm.core.api.model.Property;
56 import org.nuxeo.ecm.core.api.model.PropertyException;
58 import org.nuxeo.ecm.core.schema.types.Schema;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62 import org.w3c.dom.Document;
65 * RemoteDocumentModelHandler
67 * $LastChangedRevision: $
72 public abstract class RemoteDocumentModelHandlerImpl<T, TL>
73 extends DocumentModelHandler<T, TL> {
76 private final Logger logger = LoggerFactory.getLogger(RemoteDocumentModelHandlerImpl.class);
79 * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#setServiceContext(org.collectionspace.services.common.context.ServiceContext)
82 public void setServiceContext(ServiceContext ctx) { //FIXME: Apply proper generics to ServiceContext<MultipartInput, MultipartOutput>
83 if (ctx instanceof MultipartServiceContext) {
84 super.setServiceContext(ctx);
86 throw new IllegalArgumentException("setServiceContext requires instance of "
87 + MultipartServiceContext.class.getName());
92 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#completeUpdate(org.collectionspace.services.common.document.DocumentWrapper)
95 public void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
96 DocumentModel docModel = wrapDoc.getWrappedObject();
97 //return at least those document part(s) that were received
98 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
99 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
100 MultipartInput input = ctx.getInput();
102 List<InputPart> inputParts = ctx.getInput().getParts();
103 for (InputPart part : inputParts) {
104 String partLabel = part.getHeaders().getFirst("label");
105 ObjectPartType partMeta = partsMetaMap.get(partLabel);
106 // extractPart(docModel, partLabel, partMeta);
107 Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
108 addOutputPart(unQObjectProperties, partLabel, partMeta);
111 if (logger.isWarnEnabled() == true) {
112 logger.warn("MultipartInput part was null for document id = " +
119 * Adds the output part.
121 * @param unQObjectProperties the un q object properties
122 * @param schema the schema
123 * @param partMeta the part meta
124 * @throws Exception the exception
126 private void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
128 Document doc = DocumentUtils.buildDocument(partMeta, schema,
129 unQObjectProperties);
130 if (logger.isDebugEnabled() == true) {
131 logger.debug(DocumentUtils.xmlToString(doc));
133 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
134 ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
138 * Extract paging info.
140 * @param commonsList the commons list
142 * @throws Exception the exception
144 public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
146 AbstractCommonList commonList = (AbstractCommonList) theCommonList;
148 DocumentFilter docFilter = this.getDocumentFilter();
149 long pageSize = docFilter.getPageSize();
150 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
151 // set the page size and page number
152 commonList.setPageNum(pageNum);
153 commonList.setPageSize(pageSize);
154 DocumentModelList docList = wrapDoc.getWrappedObject();
155 // Set num of items in list. this is useful to our testing framework.
156 commonList.setItemsInPage(docList.size());
157 // set the total result size
158 commonList.setTotalItems(docList.totalSize());
160 return (TL) commonList;
164 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
167 public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
170 DocumentModel docModel = wrapDoc.getWrappedObject();
171 String[] schemas = docModel.getDeclaredSchemas();
172 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
173 for (String schema : schemas) {
174 ObjectPartType partMeta = partsMetaMap.get(schema);
175 if (partMeta == null) {
176 continue; // unknown part, ignore
178 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
179 addOutputPart(unQObjectProperties, schema, partMeta);
184 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
187 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
189 //TODO filling extension parts should be dynamic
190 //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
191 //not an ideal way of populating objects.
192 DocumentModel docModel = wrapDoc.getWrappedObject();
193 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
194 MultipartInput input = ctx.getInput();
195 if (input.getParts().isEmpty()) {
196 String msg = "No payload found!";
197 logger.error(msg + "Ctx=" + getServiceContext().toString());
198 throw new BadRequestException(msg);
201 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
203 //iterate over parts received and fill those parts
204 List<InputPart> inputParts = input.getParts();
205 for (InputPart part : inputParts) {
207 String partLabel = part.getHeaders().getFirst("label");
208 if (partLabel == null) {
209 String msg = "Part label is missing or empty!";
210 logger.error(msg + "Ctx=" + getServiceContext().toString());
211 throw new BadRequestException(msg);
214 //skip if the part is not in metadata
215 ObjectPartType partMeta = partsMetaMap.get(partLabel);
216 if (partMeta == null) {
219 fillPart(part, docModel, partMeta, action, ctx);
225 * fillPart fills an XML part into given document model
226 * @param part to fill
227 * @param docModel for the given object
228 * @param partMeta metadata for the object to fill
231 protected void fillPart(InputPart part, DocumentModel docModel,
232 ObjectPartType partMeta, Action action, ServiceContext ctx)
234 InputStream payload = part.getBody(InputStream.class, null);
236 //check if this is an xml part
237 if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
238 if (payload != null) {
239 Document document = DocumentUtils.parseDocument(payload, partMeta,
240 false /*don't validate*/);
241 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, document, ctx);
242 if (action == Action.UPDATE) {
243 this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
245 docModel.setProperties(partMeta.getLabel(), objectProps);
251 * Filters out read only properties, so they cannot be set on update.
252 * TODO: add configuration support to do this generally
253 * @param objectProps the properties parsed from the update payload
254 * @param partMeta metadata for the object to fill
256 public void filterReadOnlyPropertiesForPart(
257 Map<String, Object> objectProps, ObjectPartType partMeta) {
258 // Currently a no-op, but can be overridden in Doc handlers.
262 * extractPart extracts an XML object from given DocumentModel
264 * @param schema of the object to extract
265 * @param partMeta metadata for the object to extract
268 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
270 return extractPart(docModel, schema, partMeta, null);
274 * extractPart extracts an XML object from given DocumentModel
276 * @param schema of the object to extract
277 * @param partMeta metadata for the object to extract
280 protected Map<String, Object> extractPart(
281 DocumentModel docModel, String schema, ObjectPartType partMeta,
282 Map<String, Object> addToMap)
284 Map<String, Object> result = null;
286 MediaType mt = MediaType.valueOf(partMeta.getContent().getContentType());
287 if (mt.equals(MediaType.APPLICATION_XML_TYPE)) {
288 Map<String, Object> objectProps = docModel.getProperties(schema);
289 //unqualify properties before sending the doc over the wire (to save bandwidh)
290 //FIXME: is there a better way to avoid duplication of a collection?
291 Map<String, Object> unQObjectProperties =
292 (addToMap != null) ? addToMap : (new HashMap<String, Object>());
293 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
294 for (Entry<String, Object> entry : qualifiedEntries) {
295 String unqProp = getUnQProperty(entry.getKey());
296 unQObjectProperties.put(unqProp, entry.getValue());
298 result = unQObjectProperties;
299 } //TODO: handle other media types
305 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
308 public AuthorityRefList getAuthorityRefs(
309 DocumentWrapper<DocumentModel> docWrapper,
310 List<String> authRefFieldNames) throws PropertyException {
312 AuthorityRefList authRefList = new AuthorityRefList();
313 AbstractCommonList commonList = (AbstractCommonList) authRefList;
315 DocumentFilter docFilter = this.getDocumentFilter();
316 long pageSize = docFilter.getPageSize();
317 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
318 // set the page size and page number
319 commonList.setPageNum(pageNum);
320 commonList.setPageSize(pageSize);
322 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
323 DocumentModel docModel = docWrapper.getWrappedObject();
326 int iFirstToUse = (int)(pageSize*pageNum);
327 int nFoundInPage = 0;
329 for (String authRefFieldName : authRefFieldNames) {
331 // FIXME: Can use the schema to validate field existence,
332 // to help avoid encountering PropertyExceptions.
333 String schemaName = DocumentUtils.getSchemaNamePart(authRefFieldName);
334 Schema schema = DocumentUtils.getSchemaFromName(schemaName);
336 String descendantAuthRefFieldName = DocumentUtils.getDescendantAuthRefFieldName(authRefFieldName);
337 if (descendantAuthRefFieldName != null && !descendantAuthRefFieldName.trim().isEmpty()) {
338 authRefFieldName = DocumentUtils.getAncestorAuthRefFieldName(authRefFieldName);
341 String xpath = "//" + authRefFieldName;
342 Property prop = docModel.getProperty(xpath);
347 // If this is a single scalar field, with no children,
348 // add an item with its values to the authRefs list.
349 if (DocumentUtils.isSimpleType(prop)) {
350 String refName = prop.getValue(String.class);
351 if (refName == null) {
354 refName = refName.trim();
355 if (refName.isEmpty()) {
358 if((nFoundTotal < iFirstToUse)
359 || (nFoundInPage >= pageSize)) {
365 appendToAuthRefsList(refName, schemaName, authRefFieldName, list);
367 // Otherwise, if this field has children, cycle through each child.
369 // Whenever we find instances of the descendant field among
370 // these children, add an item with its values to the authRefs list.
372 // FIXME: When we increase maximum repeatability depth, that is, the depth
373 // between ancestor and descendant, we'll need to use recursion here,
374 // rather than making fixed assumptions about hierarchical depth.
375 } else if ((DocumentUtils.isListType(prop) || DocumentUtils.isComplexType(prop))
376 && prop.size() > 0) {
378 Collection<Property> childProp = prop.getChildren();
379 for (Property cProp : childProp) {
380 if (DocumentUtils.isSimpleType(cProp) && cProp.getName().equals(descendantAuthRefFieldName)) {
381 String refName = cProp.getValue(String.class);
382 if (refName == null) {
385 refName = refName.trim();
386 if (refName.isEmpty()) {
389 if((nFoundTotal < iFirstToUse)
390 || (nFoundInPage >= pageSize)) {
396 appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
397 } else if ((DocumentUtils.isListType(cProp) || DocumentUtils.isComplexType(cProp))
398 && prop.size() > 0) {
399 Collection<Property> grandChildProp = cProp.getChildren();
400 for (Property gProp : grandChildProp) {
401 if (DocumentUtils.isSimpleType(gProp) && gProp.getName().equals(descendantAuthRefFieldName)) {
402 String refName = gProp.getValue(String.class);
403 if (refName == null) {
406 refName = refName.trim();
407 if (refName.isEmpty()) {
410 if((nFoundTotal < iFirstToUse)
411 || (nFoundInPage >= pageSize)) {
417 appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
424 // Set num of items in list. this is useful to our testing framework.
425 commonList.setItemsInPage(nFoundInPage);
426 // set the total result size
427 commonList.setTotalItems(nFoundTotal);
429 } catch (PropertyException pe) {
430 String msg = "Attempted to retrieve value for invalid or missing authority field. "
431 + "Check authority field properties in tenant bindings.";
432 logger.warn(msg, pe);
434 } catch (Exception e) {
435 if (logger.isDebugEnabled()) {
436 logger.debug("Caught exception in getAuthorityRefs", e);
438 Response response = Response.status(
439 Response.Status.INTERNAL_SERVER_ERROR).entity(
440 "Failed to retrieve authority references").type(
441 "text/plain").build();
442 throw new WebApplicationException(response);
448 private void appendToAuthRefsList(String refName, String schemaName,
449 String fieldName, List<AuthorityRefList.AuthorityRefItem> list)
451 if (DocumentUtils.getSchemaNamePart(fieldName).isEmpty()) {
452 fieldName = DocumentUtils.appendSchemaName(schemaName, fieldName);
454 list.add(authorityRefListItem(fieldName, refName));
457 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
459 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
461 RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
462 ilistItem.setRefName(refName);
463 ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
464 ilistItem.setItemDisplayName(termInfo.displayName);
465 ilistItem.setSourceField(authRefFieldName);
466 ilistItem.setUri(termInfo.getRelativeUri());
467 } catch (Exception e) {
468 // Do nothing upon encountering an Exception here.
474 * Returns the primary value from a list of values.
476 * Assumes that the first value is the primary value.
477 * This assumption may change when and if the primary value
478 * is identified explicitly.
480 * @param values a list of values.
481 * @param propertyName the name of a property through
482 * which the value can be extracted.
483 * @return the primary value.
484 protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
485 String primaryValue = "";
486 if (values == null || values.size() == 0) {
489 Object value = values.get(0);
490 if (value instanceof String) {
492 primaryValue = (String) value;
494 // Multivalue group of fields
495 } else if (value instanceof Map) {
497 Map map = (Map) value;
498 if (map.values().size() > 0) {
499 if (map.get(propertyName) != null) {
500 primaryValue = (String) map.get(propertyName);
505 logger.warn("Unexpected type for property " + propertyName
506 + " in multivalue list: not String or Map.");
513 * Gets a simple property from the document.
515 * For completeness, as this duplicates DocumentModel method.
517 * @param docModel The document model to get info from
518 * @param schema The name of the schema (part)
519 * @param propertyName The simple scalar property type
520 * @return property value as String
522 protected String getSimpleStringProperty(DocumentModel docModel, String schema, String propName) {
523 String xpath = "/"+schema+":"+propName;
525 return (String)docModel.getPropertyValue(xpath);
526 } catch(PropertyException pe) {
527 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a simple String property?"
528 +pe.getLocalizedMessage());
529 } catch(ClassCastException cce) {
530 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a scalar String property?"
531 +cce.getLocalizedMessage());
532 } catch(Exception e) {
533 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
534 +e.getLocalizedMessage());
539 * Gets first of a repeating list of scalar values, as a String, from the document.
541 * @param docModel The document model to get info from
542 * @param schema The name of the schema (part)
543 * @param listName The name of the scalar list property
544 * @return first value in list, as a String, or empty string if the list is empty
546 protected String getFirstRepeatingStringProperty(
547 DocumentModel docModel, String schema, String listName) {
548 String xpath = "/"+schema+":"+listName+"/[0]";
550 return (String)docModel.getPropertyValue(xpath);
551 } catch(PropertyException pe) {
552 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Not a repeating scalar?"
553 +pe.getLocalizedMessage());
554 } catch(IndexOutOfBoundsException ioobe) {
555 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
556 return ""; // gracefully handle missing elements
557 } catch(ClassCastException cce) {
558 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a repeating String property?"
559 +cce.getLocalizedMessage());
560 } catch(Exception e) {
561 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
562 +e.getLocalizedMessage());
568 * Gets first of a repeating list of scalar values, as a String, from the document.
570 * @param docModel The document model to get info from
571 * @param schema The name of the schema (part)
572 * @param listName The name of the scalar list property
573 * @return first value in list, as a String, or empty string if the list is empty
575 protected String getStringValueInPrimaryRepeatingComplexProperty(
576 DocumentModel docModel, String schema, String complexPropertyName, String fieldName) {
577 String xpath = "/"+schema+":"+complexPropertyName+"/[0]/"+fieldName;
579 return (String)docModel.getPropertyValue(xpath);
580 } catch(PropertyException pe) {
581 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad propertyNames?"
582 +pe.getLocalizedMessage());
583 } catch(IndexOutOfBoundsException ioobe) {
584 // Nuxeo sometimes handles missing sub, and sometimes does not. Odd.
585 return ""; // gracefully handle missing elements
586 } catch(ClassCastException cce) {
587 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
588 +cce.getLocalizedMessage());
589 } catch(Exception e) {
590 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
591 +e.getLocalizedMessage());
596 * Gets XPath value from schema. Note that only "/" and "[n]" are
597 * supported for xpath. Can omit grouping elements for repeating complex types,
598 * e.g., "fieldList/[0]" can be used as shorthand for "fieldList/field[0]" and
599 * "fieldGroupList/[0]/field" can be used as shorthand for "fieldGroupList/fieldGroup[0]/field".
600 * If there are no entries for a list of scalars or for a list of complex types,
601 * a 0 index expression (e.g., "fieldGroupList/[0]/field") will safely return an empty
602 * string. A non-zero index will throw an IndexOutOfBoundsException if there are not
603 * that many elements in the list.
604 * N.B.: This does not follow the XPath spec - indices are 0-based, not 1-based.
606 * @param docModel The document model to get info from
607 * @param schema The name of the schema (part)
608 * @param xpath The XPath expression (without schema prefix)
609 * @return value the indicated property value as a String
611 protected static String getXPathStringValue(DocumentModel docModel, String schema, String xpath) {
612 xpath = schema+":"+xpath;
614 return (String)docModel.getPropertyValue(xpath);
615 } catch(PropertyException pe) {
616 throw new RuntimeException("Problem retrieving property {"+xpath+"}. Bad XPath spec?"
617 +pe.getLocalizedMessage());
618 } catch(ClassCastException cce) {
619 throw new RuntimeException("Problem retrieving property {"+xpath+"} as String. Not a String property?"
620 +cce.getLocalizedMessage());
621 } catch(IndexOutOfBoundsException ioobe) {
622 // Nuxeo seems to handle foo/[0]/bar when it is missing,
623 // but not foo/bar[0] (for repeating scalars).
624 if(xpath.endsWith("[0]")) { // gracefully handle missing elements
627 String msg = ioobe.getMessage();
628 if(msg!=null && msg.equals("Index: 0, Size: 0")) {
629 // Some other variant on a missing sub-field; quietly absorb.
631 } // Otherwise, e.g., for true OOB indices, propagate the exception.
633 throw new RuntimeException("Problem retrieving property {"+xpath+"}:"
634 +ioobe.getLocalizedMessage());
635 } catch(Exception e) {
636 throw new RuntimeException("Unknown problem retrieving property {"+xpath+"}."
637 +e.getLocalizedMessage());