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.vocabulary;
26 import org.collectionspace.services.client.IClientQueryParams;
27 import org.collectionspace.services.client.PayloadInputPart;
28 import org.collectionspace.services.client.PoxPayload;
29 import org.collectionspace.services.client.PoxPayloadIn;
30 import org.collectionspace.services.client.PoxPayloadOut;
31 import org.collectionspace.services.client.VocabularyClient;
32 import org.collectionspace.services.client.workflow.WorkflowClient;
33 import org.collectionspace.services.common.CSWebApplicationException;
34 import org.collectionspace.services.common.ResourceMap;
35 import org.collectionspace.services.common.ServiceMessages;
36 import org.collectionspace.services.common.UriInfoWrapper;
37 import org.collectionspace.services.common.api.Tools;
38 import org.collectionspace.services.common.context.ServiceBindingUtils;
39 import org.collectionspace.services.common.context.ServiceContext;
40 import org.collectionspace.services.common.document.DocumentException;
41 import org.collectionspace.services.common.document.DocumentHandler;
42 import org.collectionspace.services.common.document.DocumentNotFoundException;
43 import org.collectionspace.services.common.repository.RepositoryClient;
44 import org.collectionspace.services.common.vocabulary.AuthorityItemJAXBSchema;
45 import org.collectionspace.services.common.vocabulary.AuthorityResource;
46 import org.collectionspace.services.common.vocabulary.AuthorityServiceUtils;
47 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
48 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.Specifier;
49 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.SpecifierForm;
50 import org.collectionspace.services.common.vocabulary.nuxeo.AuthorityIdentifierUtils;
51 import org.collectionspace.services.jaxb.AbstractCommonList;
52 import org.collectionspace.services.jaxb.AbstractCommonList.ListItem;
53 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
54 import org.collectionspace.services.vocabulary.nuxeo.VocabularyItemDocumentModelHandler;
55 import org.nuxeo.ecm.core.api.DocumentModel;
56 import org.nuxeo.ecm.core.api.DocumentModelList;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59 import org.w3c.dom.Element;
61 import java.util.ArrayList;
62 import java.util.Base64;
63 import java.util.HashSet;
66 import javax.ws.rs.GET;
67 import javax.ws.rs.POST;
68 import javax.ws.rs.PUT;
69 import javax.ws.rs.Path;
70 import javax.ws.rs.PathParam;
71 import javax.ws.rs.core.Context;
72 import javax.ws.rs.core.MultivaluedMap;
73 import javax.ws.rs.core.Request;
74 import javax.ws.rs.core.Response;
75 import javax.ws.rs.core.UriBuilder;
76 import javax.ws.rs.core.UriInfo;
77 import javax.xml.bind.DatatypeConverter;
79 @Path("/" + VocabularyClient.SERVICE_PATH_COMPONENT)
80 public class VocabularyResource extends
81 AuthorityResource<VocabulariesCommon, VocabularyItemDocumentModelHandler> {
87 private final static String vocabularyServiceName = VocabularyClient.SERVICE_PATH_COMPONENT;
89 private final static String VOCABULARIES_COMMON = "vocabularies_common";
91 private final static String vocabularyItemServiceName = "vocabularyitems";
92 private final static String VOCABULARYITEMS_COMMON = "vocabularyitems_common";
94 final Logger logger = LoggerFactory.getLogger(VocabularyResource.class);
96 public VocabularyResource() {
97 super(VocabulariesCommon.class, VocabularyResource.class,
98 VOCABULARIES_COMMON, VOCABULARYITEMS_COMMON);
103 public Response createAuthority(
104 @Context ResourceMap resourceMap,
105 @Context UriInfo uriInfo,
108 // Requests to create new authorities come in on new threads. Unfortunately, we need to synchronize those threads on this block because, as of 8/27/2015, we can't seem to get Nuxeo
109 // transaction code to deal with a database level UNIQUE constraint violations on the 'shortidentifier' column of the vocabularies_common table.
110 // Therefore, to prevent having multiple authorities with the same shortid, we need to synchronize
111 // the code that creates new authorities. The authority document model handler will first check for authorities with the same short id before
112 // trying to create a new authority.
114 synchronized(AuthorityResource.class) {
116 PoxPayloadIn input = new PoxPayloadIn(xmlPayload);
117 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(input);
118 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = this.getRepositoryClient(ctx);
120 CoreSessionInterface repoSession = repoClient.getRepositorySession(ctx);
122 DocumentHandler<?, AbstractCommonList, DocumentModel, DocumentModelList> handler = createDocumentHandler(ctx);
123 String csid = repoClient.create(ctx, handler);
125 // Handle any supplied list of items/terms
127 handleItemsPayload(Method.POST, ctx, csid, resourceMap, uriInfo, input);
128 UriBuilder path = UriBuilder.fromResource(resourceClass);
129 path.path("" + csid);
130 Response response = Response.created(path.build()).build();
132 } catch (Throwable t) {
133 repoSession.setTransactionRollbackOnly();
136 repoClient.releaseRepositorySession(ctx, repoSession);
138 } catch (Exception e) {
139 throw bigReThrow(e, ServiceMessages.CREATE_FAILED);
147 public byte[] updateAuthority(
148 @Context Request request,
149 @Context ResourceMap resourceMap,
151 @PathParam("csid") String specifier,
153 PoxPayloadOut result = null;
155 UriInfoWrapper uriInfo = new UriInfoWrapper(ui); // We need to make the queryParams maps read-write instead of read-only
156 PoxPayloadIn theUpdate = new PoxPayloadIn(xmlPayload);
157 Specifier spec = Specifier.getSpecifier(specifier, "updateAuthority", "UPDATE");
158 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(theUpdate, uriInfo);
159 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = this.getRepositoryClient(ctx);
161 CoreSessionInterface repoSession = repoClient.getRepositorySession(ctx);
163 DocumentHandler<?, AbstractCommonList, DocumentModel, DocumentModelList> handler = createDocumentHandler(ctx);
165 if (spec.form == SpecifierForm.CSID) {
168 String whereClause = RefNameServiceUtils.buildWhereForAuthByName(authorityCommonSchemaName, spec.value);
169 csid = getRepositoryClient(ctx).findDocCSID(null, ctx, whereClause);
171 getRepositoryClient(ctx).update(ctx, csid, handler);
172 if (handleItemsPayload(Method.PUT, ctx, csid, resourceMap, uriInfo, theUpdate) == true) {
173 ctx.setOutput(new PoxPayloadOut(getServiceName())); // Clear the "vocabularies_common" result since we're going to create a new one with the items-list payload
174 result = this.getAuthority(ctx, request, uriInfo, specifier, true);
176 result = ctx.getOutput();
178 } catch (Throwable t) {
179 repoSession.setTransactionRollbackOnly();
182 repoClient.releaseRepositorySession(ctx, repoSession);
184 } catch (Exception e) {
185 throw bigReThrow(e, ServiceMessages.UPDATE_FAILED);
187 return result.getBytes();
190 private void updateWithItemsPayload(
191 AbstractCommonList itemsList,
192 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
193 String parentIdentifier,
194 ResourceMap resourceMap,
196 PoxPayloadIn input) throws Exception {
198 CoreSessionInterface repoSession = (CoreSessionInterface) existingCtx.getCurrentRepositorySession();
199 Set<String> shortIdsInPayload = getListOfShortIds(itemsList); // record the list of existing or new terms/items
202 // First try to update and/or create items in the incoming payload
204 for (ListItem item : itemsList.getListItem()) {
205 String errMsg = null;
206 boolean success = true;
207 Response response = null;
208 PoxPayloadOut payloadOut = null;
209 PoxPayloadIn itemXmlPayload = getItemXmlPayload(item);
210 String itemSpecifier = getSpecifier(item);
211 if (itemSpecifier != null) {
213 payloadOut = updateAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemSpecifier, itemXmlPayload);
214 if (payloadOut == null) {
216 errMsg = String.format("Could not update the term list payload of vocabuary '%s'.", parentIdentifier);
218 } catch (DocumentNotFoundException dnf) {
220 // Since the item doesn't exist, we're being ask to create it
222 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
223 if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
225 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
230 // Since the item was supplied with neither a CSID nor a short identifier, we'll assume we're being
231 // asked to create it.
233 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
234 if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
235 String shortId = getShortId(itemXmlPayload);
236 shortIdsInPayload.add(shortId); // add the new short ID to the list of incoming items
239 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
243 // Throw an exception as soon as we have problems with any item
245 if (success == false) {
246 throw new DocumentException(errMsg);
251 // Next, delete the items that were omitted from the incoming payload
253 if (shouldDeleteOmittedItems(uriInfo) == true) {
254 UriInfo uriInfoCopy = new UriInfoWrapper(uriInfo);
255 String omittedItemAction = getOmittedItemAction(uriInfoCopy);
256 long itemsProcessed = 0;
257 long currentPage = 0;
259 AbstractCommonList abstractCommonList = this.getAuthorityItemList(existingCtx, parentIdentifier, uriInfoCopy);
260 if (abstractCommonList != null && !Tools.isEmpty(abstractCommonList.getListItem())) {
261 if (omittedItemAction.equalsIgnoreCase(VocabularyClient.DELETE_OMITTED_ITEMS)) {
262 deleteAuthorityItems(existingCtx, abstractCommonList, shortIdsInPayload, parentIdentifier);
264 sotfDeleteAuthorityItems(existingCtx, abstractCommonList, shortIdsInPayload, parentIdentifier);
267 itemsProcessed = itemsProcessed + abstractCommonList.getItemsInPage();
268 if (itemsProcessed >= abstractCommonList.getTotalItems()) {
271 ArrayList<String> pageNum = new ArrayList<String>();
272 pageNum.add(Long.toString(++currentPage));
273 uriInfoCopy.getQueryParameters().put(IClientQueryParams.START_PAGE_PARAM, pageNum);
278 private String getShortId(PoxPayloadIn itemXmlPayload) {
279 String result = null;
281 VocabularyitemsCommon vocabularyItemsCommon = (VocabularyitemsCommon) itemXmlPayload.getPart(VOCABULARYITEMS_COMMON).getBody();
282 result = vocabularyItemsCommon.getShortIdentifier();
287 private void deleteAuthorityItems(
288 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
289 AbstractCommonList abstractCommonList,
290 Set<String> shortIdsInPayload,
291 String parentIdentifier) throws Exception {
293 for (ListItem item : abstractCommonList.getListItem()) {
294 String shortId = getShortId(item);
295 if (shortIdsInPayload.contains(shortId) == false) {
296 deleteAuthorityItem(existingCtx, parentIdentifier, getCsid(item), AuthorityServiceUtils.UPDATE_REV);
301 private void sotfDeleteAuthorityItems(
302 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
303 AbstractCommonList abstractCommonList,
304 Set<String> shortIdsInPayload,
305 String parentIdentifier) throws Exception {
307 for (ListItem item : abstractCommonList.getListItem()) {
308 String shortId = getShortId(item);
309 if (shortIdsInPayload.contains(shortId) == false) {
310 //deleteAuthorityItem(existingCtx, parentIdentifier, getCsid(item), AuthorityServiceUtils.UPDATE_REV);
311 this.updateItemWorkflowWithTransition(existingCtx, parentIdentifier, getCsid(item),
312 WorkflowClient.WORKFLOWTRANSITION_DELETE, AuthorityServiceUtils.UPDATE_REV);
317 private boolean shouldDeleteOmittedItems(UriInfo uriInfo) throws DocumentException {
318 boolean result = false;
320 String omittedItemAction = getOmittedItemAction(uriInfo);
321 if (Tools.isEmpty(omittedItemAction) == false) {
322 switch (omittedItemAction) {
323 case VocabularyClient.DELETE_OMITTED_ITEMS:
324 case VocabularyClient.SOFTDELETE_OMITTED_ITEMS:
327 case VocabularyClient.IGNORE_OMITTED_ITEMS:
331 String msg = String.format("Unknown value '%s' for update on a vocabulary/termlist resource.", omittedItemAction);
332 throw new DocumentException(msg);
339 private String getOmittedItemAction(UriInfo uriInfo) {
340 MultivaluedMap<String,String> queryParams = uriInfo.getQueryParameters();
341 String omittedItemAction = queryParams.getFirst(VocabularyClient.OMITTED_ITEM_ACTION_QP);
342 return omittedItemAction;
346 * Returns the set of short identifiers in the abstract common list of authority items
348 private Set<String> getListOfShortIds(AbstractCommonList itemsList) {
349 HashSet<String> result = new HashSet<String>();
351 for (ListItem item : itemsList.getListItem()) {
352 String shortId = getShortId(item);
353 if (Tools.isEmpty(shortId) == false) {
361 private void createWithItemsPayload(
362 AbstractCommonList itemsList,
363 ServiceContext<PoxPayloadIn,
364 PoxPayloadOut> existingCtx,
365 String parentIdentifier,
366 ResourceMap resourceMap,
368 PoxPayloadIn input) throws Exception {
370 for (ListItem item : itemsList.getListItem()) {
371 String errMsg = null;
372 boolean success = true;
373 Response response = null;
374 PoxPayloadIn itemXmlPayload = getItemXmlPayload(item);
376 CoreSessionInterface repoSession = (CoreSessionInterface) existingCtx.getCurrentRepositorySession();
377 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
378 if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
380 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
383 // Throw an exception as soon as we have problems with any item
385 if (success == false) {
386 throw new DocumentException(errMsg);
391 private boolean handleItemsPayload(
393 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
394 String parentIdentifier,
395 ResourceMap resourceMap,
397 PoxPayloadIn input) throws Exception {
398 boolean result = false;
400 PayloadInputPart abstractCommonListPart = input.getPart(PoxPayload.ABSTRACT_COMMON_LIST_ROOT_ELEMENT_LABEL);
401 if (abstractCommonListPart != null) {
402 AbstractCommonList itemsList = (AbstractCommonList) abstractCommonListPart.getBody();
405 createWithItemsPayload(itemsList, existingCtx, parentIdentifier, resourceMap, uriInfo, input);
408 updateWithItemsPayload(itemsList, existingCtx, parentIdentifier, resourceMap, uriInfo, input);
411 result = true; // mark that we've handled an items-list payload
417 private String getFieldValue(ListItem item, String lookingFor) {
418 String result = null;
420 for (Element ele : item.getAny()) {
421 String fieldName = ele.getTagName();
422 String fieldValue = ele.getTextContent();
423 if (fieldName.equalsIgnoreCase(lookingFor)) {
432 public String getCsid(ListItem item) {
433 return getFieldValue(item, "csid");
436 private String getShortId(ListItem item) {
437 return getFieldValue(item, "shortIdentifier");
440 private String getDisplayName(ListItem item) {
441 return getFieldValue(item, "displayName");
445 * We'll return null if we can create a specifier from the list item.
450 private String getSpecifier(ListItem item) {
451 String result = null;
453 String csid = result = getCsid(item);
455 String shortId = getShortId(item);
456 if (shortId != null) {
457 result = Specifier.createShortIdURNValue(shortId);
465 * This is very brittle. If the class VocabularyitemsCommon changed with new fields we'd have to
466 * update this method.
470 * @throws DocumentException
472 private PoxPayloadIn getItemXmlPayload(ListItem item) throws DocumentException {
473 PoxPayloadIn result = null;
475 VocabularyitemsCommon vocabularyItem = new VocabularyitemsCommon();
476 for (Element ele : item.getAny()) {
477 String fieldName = ele.getTagName();
478 String fieldValue = ele.getTextContent();
481 vocabularyItem.setDisplayName(fieldValue);
484 case "shortIdentifier":
485 vocabularyItem.setShortIdentifier(fieldValue);
489 vocabularyItem.setOrder(fieldValue);
493 vocabularyItem.setSource(fieldValue);
497 vocabularyItem.setSourcePage(fieldValue);
501 vocabularyItem.setDescription(fieldValue);
505 vocabularyItem.setCsid(fieldValue);
509 vocabularyItem.setTermStatus(fieldValue);
513 // ignore other fields
518 // We need to create a short ID if one wasn't supplied
520 if (Tools.isEmpty(vocabularyItem.getShortIdentifier())) {
521 vocabularyItem.setShortIdentifier(AuthorityIdentifierUtils.generateShortIdentifierFromDisplayName(
522 vocabularyItem.getDisplayName() , null)); ;
525 result = new PoxPayloadIn(VocabularyClient.SERVICE_ITEM_PAYLOAD_NAME, vocabularyItem,
526 VOCABULARYITEMS_COMMON);
531 private Response createAuthorityItem(
532 CoreSessionInterface repoSession,
533 ResourceMap resourceMap,
535 String parentIdentifier, // Either a CSID or a URN form -e.g., a8ad38ec-1d7d-4bf2-bd31 or urn:cspace:name(bugsbunny)
536 PoxPayloadIn input) throws Exception {
537 Response result = null;
539 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(getItemServiceName(), input, resourceMap, uriInfo);
540 ctx.setCurrentRepositorySession(repoSession);
542 result = createAuthorityItem(ctx, parentIdentifier, AuthorityServiceUtils.UPDATE_REV,
543 AuthorityServiceUtils.PROPOSED, AuthorityServiceUtils.NOT_SAS_ITEM);
548 private PoxPayloadOut updateAuthorityItem(
549 CoreSessionInterface repoSession,
550 ResourceMap resourceMap,
552 String parentSpecifier, // Either a CSID or a URN form -e.g., a8ad38ec-1d7d-4bf2-bd31 or urn:cspace:name(bugsbunny)
553 String itemSpecifier, // Either a CSID or a URN form.
554 PoxPayloadIn theUpdate) throws Exception {
555 PoxPayloadOut result = null;
557 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(getItemServiceName(), theUpdate, resourceMap, uriInfo);
558 ctx.setCurrentRepositorySession(repoSession);
560 result = updateAuthorityItem(ctx, resourceMap, uriInfo, parentSpecifier, itemSpecifier, theUpdate,
561 AuthorityServiceUtils.UPDATE_REV, // passing TRUE so rev num increases, passing
562 AuthorityServiceUtils.NO_CHANGE, // don't change the state of the "proposed" field -we could be performing a sync or just a plain update
563 AuthorityServiceUtils.NO_CHANGE); // don't change the state of the "sas" field -we could be performing a sync or just a plain update
572 @Context Request request,
573 @Context UriInfo uriInfo,
574 @PathParam("csid") String specifier) {
575 Response result = null;
576 uriInfo = new UriInfoWrapper(uriInfo);
579 MultivaluedMap<String,String> queryParams = uriInfo.getQueryParameters();
580 String showItemsValue = (String)queryParams.getFirst(VocabularyClient.SHOW_ITEMS_QP);
581 boolean showItems = Tools.isTrue(showItemsValue);
582 if (showItems == true) {
584 // We'll honor paging params if we find any; otherwise we'll set the page size to 0 to get ALL the items
586 if (queryParams.containsKey(IClientQueryParams.PAGE_SIZE_PARAM) == false) {
587 queryParams.add(IClientQueryParams.PAGE_SIZE_PARAM, "0");
591 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(request, uriInfo);
592 PoxPayloadOut payloadout = getAuthority(ctx, request, uriInfo, specifier, showItems);
593 result = buildResponse(ctx, payloadout);
594 } catch (Exception e) {
595 throw bigReThrow(e, ServiceMessages.GET_FAILED, specifier);
598 if (result == null) {
599 Response response = Response.status(Response.Status.NOT_FOUND).entity(
600 "GET request failed. The requested Authority specifier:" + specifier + ": was not found.").type(
601 "text/plain").build();
602 throw new CSWebApplicationException(response);
609 public String getServiceName() {
610 return vocabularyServiceName;
614 public String getItemServiceName() {
615 return vocabularyItemServiceName;
619 public Class<VocabulariesCommon> getCommonPartClass() {
620 return VocabulariesCommon.class;
624 * @return the name of the property used to specify references for items in this type of
625 * authority. For most authorities, it is ServiceBindingUtils.AUTH_REF_PROP ("authRef").
626 * Some types (like Vocabulary) use a separate property.
629 protected String getRefPropName() {
630 return ServiceBindingUtils.TERM_REF_PROP;
634 protected String getOrderByField(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
635 String result = null;
637 result = ctx.getCommonPartLabel() + ":" + AuthorityItemJAXBSchema.DISPLAY_NAME;
643 protected String getPartialTermMatchField(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
644 return getOrderByField(ctx);
648 * The item schema for the Vocabulary service does not support a multi-valued term list. Only authorities that support
649 * term lists need to implement this method.
652 public String getItemTermInfoGroupXPathBase() {