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 List<InputPart> inputParts = ctx.getInput().getParts();
101 for (InputPart part : inputParts) {
102 String partLabel = part.getHeaders().getFirst("label");
103 ObjectPartType partMeta = partsMetaMap.get(partLabel);
104 // extractPart(docModel, partLabel, partMeta);
105 Map<String, Object> unQObjectProperties = extractPart(docModel, partLabel, partMeta);
106 addOutputPart(unQObjectProperties, partLabel, partMeta);
111 * Adds the output part.
113 * @param unQObjectProperties the un q object properties
114 * @param schema the schema
115 * @param partMeta the part meta
116 * @throws Exception the exception
118 private void addOutputPart(Map<String, Object> unQObjectProperties, String schema, ObjectPartType partMeta)
120 Document doc = DocumentUtils.buildDocument(partMeta, schema,
121 unQObjectProperties);
122 if (logger.isDebugEnabled() == true) {
123 logger.debug(DocumentUtils.xmlToString(doc));
125 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
126 ctx.addOutputPart(schema, doc, partMeta.getContent().getContentType());
130 * Extract paging info.
132 * @param commonsList the commons list
134 * @throws Exception the exception
136 public TL extractPagingInfo(TL theCommonList, DocumentWrapper<DocumentModelList> wrapDoc)
138 AbstractCommonList commonList = (AbstractCommonList) theCommonList;
140 DocumentFilter docFilter = this.getDocumentFilter();
141 long pageSize = docFilter.getPageSize();
142 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
143 // set the page size and page number
144 commonList.setPageNum(pageNum);
145 commonList.setPageSize(pageSize);
146 DocumentModelList docList = wrapDoc.getWrappedObject();
147 // Set num of items in list. this is useful to our testing framework.
148 commonList.setItemsInPage(docList.size());
149 // set the total result size
150 commonList.setTotalItems(docList.totalSize());
152 return (TL) commonList;
156 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#extractAllParts(org.collectionspace.services.common.document.DocumentWrapper)
159 public void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc)
162 DocumentModel docModel = wrapDoc.getWrappedObject();
163 String[] schemas = docModel.getDeclaredSchemas();
164 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
165 for (String schema : schemas) {
166 ObjectPartType partMeta = partsMetaMap.get(schema);
167 if (partMeta == null) {
168 continue; // unknown part, ignore
170 Map<String, Object> unQObjectProperties = extractPart(docModel, schema, partMeta);
171 addOutputPart(unQObjectProperties, schema, partMeta);
176 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#fillAllParts(org.collectionspace.services.common.document.DocumentWrapper)
179 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
181 //TODO filling extension parts should be dynamic
182 //Nuxeo APIs lack to support stream/byte[] input, get/setting properties is
183 //not an ideal way of populating objects.
184 DocumentModel docModel = wrapDoc.getWrappedObject();
185 MultipartServiceContext ctx = (MultipartServiceContext) getServiceContext();
186 MultipartInput input = ctx.getInput();
187 if (input.getParts().isEmpty()) {
188 String msg = "No payload found!";
189 logger.error(msg + "Ctx=" + getServiceContext().toString());
190 throw new BadRequestException(msg);
193 Map<String, ObjectPartType> partsMetaMap = getServiceContext().getPartsMetadata();
195 //iterate over parts received and fill those parts
196 List<InputPart> inputParts = input.getParts();
197 for (InputPart part : inputParts) {
199 String partLabel = part.getHeaders().getFirst("label");
200 if (partLabel == null) {
201 String msg = "Part label is missing or empty!";
202 logger.error(msg + "Ctx=" + getServiceContext().toString());
203 throw new BadRequestException(msg);
206 //skip if the part is not in metadata
207 ObjectPartType partMeta = partsMetaMap.get(partLabel);
208 if (partMeta == null) {
211 fillPart(part, docModel, partMeta, action, ctx);
217 * fillPart fills an XML part into given document model
218 * @param part to fill
219 * @param docModel for the given object
220 * @param partMeta metadata for the object to fill
223 protected void fillPart(InputPart part, DocumentModel docModel,
224 ObjectPartType partMeta, Action action, ServiceContext ctx)
226 InputStream payload = part.getBody(InputStream.class, null);
228 //check if this is an xml part
229 if (part.getMediaType().equals(MediaType.APPLICATION_XML_TYPE)) {
230 if (payload != null) {
231 Document document = DocumentUtils.parseDocument(payload, partMeta,
232 false /*don't validate*/);
233 Map<String, Object> objectProps = DocumentUtils.parseProperties(partMeta, document, ctx);
234 if (action == Action.UPDATE) {
235 this.filterReadOnlyPropertiesForPart(objectProps, partMeta);
237 docModel.setProperties(partMeta.getLabel(), objectProps);
243 * Filters out read only properties, so they cannot be set on update.
244 * TODO: add configuration support to do this generally
245 * @param objectProps the properties parsed from the update payload
246 * @param partMeta metadata for the object to fill
248 public void filterReadOnlyPropertiesForPart(
249 Map<String, Object> objectProps, ObjectPartType partMeta) {
250 // Currently a no-op, but can be overridden in Doc handlers.
254 * extractPart extracts an XML object from given DocumentModel
256 * @param schema of the object to extract
257 * @param partMeta metadata for the object to extract
260 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
262 return extractPart(docModel, schema, partMeta, null);
266 * extractPart extracts an XML object from given DocumentModel
268 * @param schema of the object to extract
269 * @param partMeta metadata for the object to extract
272 protected Map<String, Object> extractPart(
273 DocumentModel docModel, String schema, ObjectPartType partMeta,
274 Map<String, Object> addToMap)
276 Map<String, Object> result = null;
278 MediaType mt = MediaType.valueOf(partMeta.getContent().getContentType());
279 if (mt.equals(MediaType.APPLICATION_XML_TYPE)) {
280 Map<String, Object> objectProps = docModel.getProperties(schema);
281 //unqualify properties before sending the doc over the wire (to save bandwidh)
282 //FIXME: is there a better way to avoid duplication of a collection?
283 Map<String, Object> unQObjectProperties =
284 (addToMap != null) ? addToMap : (new HashMap<String, Object>());
285 Set<Entry<String, Object>> qualifiedEntries = objectProps.entrySet();
286 for (Entry<String, Object> entry : qualifiedEntries) {
287 String unqProp = getUnQProperty(entry.getKey());
288 unQObjectProperties.put(unqProp, entry.getValue());
290 result = unQObjectProperties;
291 } //TODO: handle other media types
297 * @see org.collectionspace.services.nuxeo.client.java.DocumentModelHandler#getAuthorityRefs(org.collectionspace.services.common.document.DocumentWrapper, java.util.List)
300 public AuthorityRefList getAuthorityRefs(
301 DocumentWrapper<DocumentModel> docWrapper,
302 List<String> authRefFieldNames) throws PropertyException {
304 AuthorityRefList authRefList = new AuthorityRefList();
305 AbstractCommonList commonList = (AbstractCommonList) authRefList;
307 DocumentFilter docFilter = this.getDocumentFilter();
308 long pageSize = docFilter.getPageSize();
309 long pageNum = pageSize != 0 ? docFilter.getOffset() / pageSize : pageSize;
310 // set the page size and page number
311 commonList.setPageNum(pageNum);
312 commonList.setPageSize(pageSize);
314 List<AuthorityRefList.AuthorityRefItem> list = authRefList.getAuthorityRefItem();
315 DocumentModel docModel = docWrapper.getWrappedObject();
318 int iFirstToUse = (int)(pageSize*pageNum);
319 int nFoundInPage = 0;
321 for (String authRefFieldName : authRefFieldNames) {
323 // FIXME: Can use the schema to validate field existence,
324 // to help avoid encountering PropertyExceptions.
325 String schemaName = DocumentUtils.getSchemaNamePart(authRefFieldName);
326 Schema schema = DocumentUtils.getSchemaFromName(schemaName);
328 String descendantAuthRefFieldName = DocumentUtils.getDescendantAuthRefFieldName(authRefFieldName);
329 if (descendantAuthRefFieldName != null && !descendantAuthRefFieldName.trim().isEmpty()) {
330 authRefFieldName = DocumentUtils.getAncestorAuthRefFieldName(authRefFieldName);
333 String xpath = "//" + authRefFieldName;
334 Property prop = docModel.getProperty(xpath);
339 // If this is a single scalar field, with no children,
340 // add an item with its values to the authRefs list.
341 if (DocumentUtils.isSimpleType(prop)) {
342 String refName = prop.getValue(String.class);
343 if (refName == null) {
346 refName = refName.trim();
347 if (refName.isEmpty()) {
350 if((nFoundTotal < iFirstToUse)
351 || (nFoundInPage >= pageSize)) {
357 appendToAuthRefsList(refName, schemaName, authRefFieldName, list);
359 // Otherwise, if this field has children, cycle through each child.
361 // Whenever we find instances of the descendant field among
362 // these children, add an item with its values to the authRefs list.
364 // FIXME: When we increase maximum repeatability depth, that is, the depth
365 // between ancestor and descendant, we'll need to use recursion here,
366 // rather than making fixed assumptions about hierarchical depth.
367 } else if ((DocumentUtils.isListType(prop) || DocumentUtils.isComplexType(prop))
368 && prop.size() > 0) {
370 Collection<Property> childProp = prop.getChildren();
371 for (Property cProp : childProp) {
372 if (DocumentUtils.isSimpleType(cProp) && cProp.getName().equals(descendantAuthRefFieldName)) {
373 String refName = cProp.getValue(String.class);
374 if (refName == null) {
377 refName = refName.trim();
378 if (refName.isEmpty()) {
381 if((nFoundTotal < iFirstToUse)
382 || (nFoundInPage >= pageSize)) {
388 appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
389 } else if ((DocumentUtils.isListType(cProp) || DocumentUtils.isComplexType(cProp))
390 && prop.size() > 0) {
391 Collection<Property> grandChildProp = cProp.getChildren();
392 for (Property gProp : grandChildProp) {
393 if (DocumentUtils.isSimpleType(gProp) && gProp.getName().equals(descendantAuthRefFieldName)) {
394 String refName = gProp.getValue(String.class);
395 if (refName == null) {
398 refName = refName.trim();
399 if (refName.isEmpty()) {
402 if((nFoundTotal < iFirstToUse)
403 || (nFoundInPage >= pageSize)) {
409 appendToAuthRefsList(refName, schemaName, descendantAuthRefFieldName, list);
416 // Set num of items in list. this is useful to our testing framework.
417 commonList.setItemsInPage(nFoundInPage);
418 // set the total result size
419 commonList.setTotalItems(nFoundTotal);
421 } catch (PropertyException pe) {
422 String msg = "Attempted to retrieve value for invalid or missing authority field. "
423 + "Check authority field properties in tenant bindings.";
424 logger.warn(msg, pe);
426 } catch (Exception e) {
427 if (logger.isDebugEnabled()) {
428 logger.debug("Caught exception in getAuthorityRefs", e);
430 Response response = Response.status(
431 Response.Status.INTERNAL_SERVER_ERROR).entity(
432 "Failed to retrieve authority references").type(
433 "text/plain").build();
434 throw new WebApplicationException(response);
440 private void appendToAuthRefsList(String refName, String schemaName,
441 String fieldName, List<AuthorityRefList.AuthorityRefItem> list)
443 if (DocumentUtils.getSchemaNamePart(fieldName).isEmpty()) {
444 fieldName = DocumentUtils.appendSchemaName(schemaName, fieldName);
446 list.add(authorityRefListItem(fieldName, refName));
449 private AuthorityRefList.AuthorityRefItem authorityRefListItem(String authRefFieldName, String refName) {
451 AuthorityRefList.AuthorityRefItem ilistItem = new AuthorityRefList.AuthorityRefItem();
453 RefNameUtils.AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(refName);
454 ilistItem.setRefName(refName);
455 ilistItem.setAuthDisplayName(termInfo.inAuthority.displayName);
456 ilistItem.setItemDisplayName(termInfo.displayName);
457 ilistItem.setSourceField(authRefFieldName);
458 ilistItem.setUri(termInfo.getRelativeUri());
459 } catch (Exception e) {
460 // Do nothing upon encountering an Exception here.
466 * Returns the primary value from a list of values.
468 * Assumes that the first value is the primary value.
469 * This assumption may change when and if the primary value
470 * is identified explicitly.
472 * @param values a list of values.
473 * @param propertyName the name of a property through
474 * which the value can be extracted.
475 * @return the primary value.
477 protected String primaryValueFromMultivalue(List<Object> values, String propertyName) {
478 String primaryValue = "";
479 if (values == null || values.size() == 0) {
482 Object value = values.get(0);
483 if (value instanceof String) {
485 primaryValue = (String) value;
487 // Multivalue group of fields
488 } else if (value instanceof Map) {
490 Map map = (Map) value;
491 if (map.values().size() > 0) {
492 if (map.get(propertyName) != null) {
493 primaryValue = (String) map.get(propertyName);
498 logger.warn("Unexpected type for property " + propertyName
499 + " in multivalue list: not String or Map.");