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.RefNameUtils;
38 import org.collectionspace.services.common.api.RefNameUtils.AuthorityTermInfo;
39 import org.collectionspace.services.common.api.Tools;
40 import org.collectionspace.services.common.context.ServiceBindingUtils;
41 import org.collectionspace.services.common.context.ServiceContext;
42 import org.collectionspace.services.common.document.DocumentException;
43 import org.collectionspace.services.common.document.DocumentHandler;
44 import org.collectionspace.services.common.document.DocumentNotFoundException;
45 import org.collectionspace.services.common.repository.RepositoryClient;
46 import org.collectionspace.services.common.vocabulary.AuthorityItemJAXBSchema;
47 import org.collectionspace.services.common.vocabulary.AuthorityResource;
48 import org.collectionspace.services.common.vocabulary.AuthorityServiceUtils;
49 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils;
50 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.Specifier;
51 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.SpecifierForm;
52 import org.collectionspace.services.common.vocabulary.nuxeo.AuthorityIdentifierUtils;
53 import org.collectionspace.services.jaxb.AbstractCommonList;
54 import org.collectionspace.services.jaxb.AbstractCommonList.ListItem;
55 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
56 import org.collectionspace.services.vocabulary.nuxeo.VocabularyItemDocumentModelHandler;
57 import org.nuxeo.ecm.core.api.DocumentModel;
58 import org.nuxeo.ecm.core.api.DocumentModelList;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.w3c.dom.Element;
63 import java.util.ArrayList;
64 import java.util.Base64;
65 import java.util.HashSet;
68 import javax.ws.rs.GET;
69 import javax.ws.rs.POST;
70 import javax.ws.rs.PUT;
71 import javax.ws.rs.Path;
72 import javax.ws.rs.PathParam;
73 import javax.ws.rs.core.Context;
74 import javax.ws.rs.core.MultivaluedMap;
75 import javax.ws.rs.core.Request;
76 import javax.ws.rs.core.Response;
77 import javax.ws.rs.core.UriBuilder;
78 import javax.ws.rs.core.UriInfo;
79 import javax.xml.bind.DatatypeConverter;
81 @Path("/" + VocabularyClient.SERVICE_PATH_COMPONENT)
82 public class VocabularyResource extends
83 AuthorityResource<VocabulariesCommon, VocabularyItemDocumentModelHandler> {
89 private final static String vocabularyServiceName = VocabularyClient.SERVICE_PATH_COMPONENT;
91 private final static String VOCABULARIES_COMMON = "vocabularies_common";
93 private final static String vocabularyItemServiceName = "vocabularyitems";
94 private final static String VOCABULARYITEMS_COMMON = "vocabularyitems_common";
96 final Logger logger = LoggerFactory.getLogger(VocabularyResource.class);
98 public VocabularyResource() {
99 super(VocabulariesCommon.class, VocabularyResource.class,
100 VOCABULARIES_COMMON, VOCABULARYITEMS_COMMON);
105 public Response createAuthority(
106 @Context ResourceMap resourceMap,
107 @Context UriInfo uriInfo,
110 // 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
111 // transaction code to deal with a database level UNIQUE constraint violations on the 'shortidentifier' column of the vocabularies_common table.
112 // Therefore, to prevent having multiple authorities with the same shortid, we need to synchronize
113 // the code that creates new authorities. The authority document model handler will first check for authorities with the same short id before
114 // trying to create a new authority.
116 synchronized(AuthorityResource.class) {
118 PoxPayloadIn input = new PoxPayloadIn(xmlPayload);
119 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(input);
120 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = this.getRepositoryClient(ctx);
122 CoreSessionInterface repoSession = repoClient.getRepositorySession(ctx);
124 DocumentHandler<?, AbstractCommonList, DocumentModel, DocumentModelList> handler = createDocumentHandler(ctx);
125 String csid = repoClient.create(ctx, handler);
127 // Handle any supplied list of items/terms
129 handleItemsPayload(Method.POST, ctx, csid, resourceMap, uriInfo, input);
130 UriBuilder path = UriBuilder.fromResource(resourceClass);
131 path.path("" + csid);
132 Response response = Response.created(path.build()).build();
134 } catch (Throwable t) {
135 repoSession.setTransactionRollbackOnly();
138 repoClient.releaseRepositorySession(ctx, repoSession);
140 } catch (Exception e) {
141 throw bigReThrow(e, ServiceMessages.CREATE_FAILED);
149 public byte[] updateAuthority(
150 @Context Request request,
151 @Context ResourceMap resourceMap,
153 @PathParam("csid") String specifier,
155 PoxPayloadOut result = null;
157 UriInfoWrapper uriInfo = new UriInfoWrapper(ui); // We need to make the queryParams maps read-write instead of read-only
158 PoxPayloadIn theUpdate = new PoxPayloadIn(xmlPayload);
159 Specifier spec = Specifier.getSpecifier(specifier, "updateAuthority", "UPDATE");
160 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(theUpdate, uriInfo);
161 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = this.getRepositoryClient(ctx);
163 CoreSessionInterface repoSession = repoClient.getRepositorySession(ctx);
165 DocumentHandler<?, AbstractCommonList, DocumentModel, DocumentModelList> handler = createDocumentHandler(ctx);
167 if (spec.form == SpecifierForm.CSID) {
170 String whereClause = RefNameServiceUtils.buildWhereForAuthByName(authorityCommonSchemaName, spec.value);
171 csid = getRepositoryClient(ctx).findDocCSID(null, ctx, whereClause);
173 getRepositoryClient(ctx).update(ctx, csid, handler);
174 if (handleItemsPayload(Method.PUT, ctx, csid, resourceMap, uriInfo, theUpdate) == true) {
175 ctx.setOutput(new PoxPayloadOut(getServiceName())); // Clear the "vocabularies_common" result since we're going to create a new one with the items-list payload
176 result = this.getAuthority(ctx, request, uriInfo, specifier, true);
178 result = ctx.getOutput();
180 } catch (Throwable t) {
181 repoSession.setTransactionRollbackOnly();
184 repoClient.releaseRepositorySession(ctx, repoSession);
186 } catch (Exception e) {
187 throw bigReThrow(e, ServiceMessages.UPDATE_FAILED);
189 return result.getBytes();
192 private void updateWithItemsPayload(
193 AbstractCommonList itemsList,
194 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
195 String parentIdentifier,
196 ResourceMap resourceMap,
198 PoxPayloadIn input) throws Exception {
200 CoreSessionInterface repoSession = (CoreSessionInterface) existingCtx.getCurrentRepositorySession();
201 Set<String> shortIdsInPayload = getListOfShortIds(itemsList); // record the list of existing or new terms/items
204 // First try to update and/or create items in the incoming payload
206 for (ListItem item : itemsList.getListItem()) {
207 String errMsg = null;
208 boolean success = true;
209 Response response = null;
210 PoxPayloadOut payloadOut = null;
211 PoxPayloadIn itemXmlPayload = getItemXmlPayload(item);
212 String itemSpecifier = getSpecifier(item);
213 if (itemSpecifier != null) {
215 payloadOut = updateAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemSpecifier, itemXmlPayload);
216 if (payloadOut == null) {
218 errMsg = String.format("Could not update the term list payload of vocabuary '%s'.", parentIdentifier);
220 } catch (DocumentNotFoundException dnf) {
222 // Since the item doesn't exist, we're being ask to create it
224 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
225 if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
227 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
232 // Since the item was supplied with neither a CSID nor a short identifier, we'll assume we're being
233 // asked to create it.
235 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
236 if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
237 String shortId = getShortId(itemXmlPayload);
238 shortIdsInPayload.add(shortId); // add the new short ID to the list of incoming items
241 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
245 // Throw an exception as soon as we have problems with any item
247 if (success == false) {
248 throw new DocumentException(errMsg);
253 // Next, delete the items that were omitted from the incoming payload
255 if (shouldDeleteOmittedItems(uriInfo) == true) {
256 UriInfo uriInfoCopy = new UriInfoWrapper(uriInfo);
257 String omittedItemAction = getOmittedItemAction(uriInfoCopy);
258 long itemsProcessed = 0;
259 long currentPage = 0;
261 AbstractCommonList abstractCommonList = this.getAuthorityItemList(existingCtx, parentIdentifier, uriInfoCopy);
262 if (abstractCommonList != null && !Tools.isEmpty(abstractCommonList.getListItem())) {
263 if (omittedItemAction.equalsIgnoreCase(VocabularyClient.DELETE_OMITTED_ITEMS)) {
264 deleteAuthorityItems(existingCtx, abstractCommonList, shortIdsInPayload, parentIdentifier);
266 sotfDeleteAuthorityItems(existingCtx, abstractCommonList, shortIdsInPayload, parentIdentifier);
269 itemsProcessed = itemsProcessed + abstractCommonList.getItemsInPage();
270 if (itemsProcessed >= abstractCommonList.getTotalItems()) {
273 ArrayList<String> pageNum = new ArrayList<String>();
274 pageNum.add(Long.toString(++currentPage));
275 uriInfoCopy.getQueryParameters().put(IClientQueryParams.START_PAGE_PARAM, pageNum);
280 private String getShortId(PoxPayloadIn itemXmlPayload) {
281 String result = null;
283 VocabularyitemsCommon vocabularyItemsCommon = (VocabularyitemsCommon) itemXmlPayload.getPart(VOCABULARYITEMS_COMMON).getBody();
284 result = vocabularyItemsCommon.getShortIdentifier();
289 private void deleteAuthorityItems(
290 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
291 AbstractCommonList abstractCommonList,
292 Set<String> shortIdsInPayload,
293 String parentIdentifier) throws Exception {
295 for (ListItem item : abstractCommonList.getListItem()) {
296 String shortId = getShortId(item);
297 if (shortIdsInPayload.contains(shortId) == false) {
298 deleteAuthorityItem(existingCtx, parentIdentifier, getCsid(item), AuthorityServiceUtils.UPDATE_REV);
303 private void sotfDeleteAuthorityItems(
304 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
305 AbstractCommonList abstractCommonList,
306 Set<String> shortIdsInPayload,
307 String parentIdentifier) throws Exception {
309 for (ListItem item : abstractCommonList.getListItem()) {
310 String shortId = getShortId(item);
311 if (shortIdsInPayload.contains(shortId) == false) {
312 //deleteAuthorityItem(existingCtx, parentIdentifier, getCsid(item), AuthorityServiceUtils.UPDATE_REV);
313 this.updateItemWorkflowWithTransition(existingCtx, parentIdentifier, getCsid(item),
314 WorkflowClient.WORKFLOWTRANSITION_DELETE, AuthorityServiceUtils.UPDATE_REV);
319 private boolean shouldDeleteOmittedItems(UriInfo uriInfo) throws DocumentException {
320 boolean result = false;
322 String omittedItemAction = getOmittedItemAction(uriInfo);
323 if (Tools.isEmpty(omittedItemAction) == false) {
324 switch (omittedItemAction) {
325 case VocabularyClient.DELETE_OMITTED_ITEMS:
326 case VocabularyClient.SOFTDELETE_OMITTED_ITEMS:
329 case VocabularyClient.IGNORE_OMITTED_ITEMS:
333 String msg = String.format("Unknown value '%s' for update on a vocabulary/termlist resource.", omittedItemAction);
334 throw new DocumentException(msg);
341 private String getOmittedItemAction(UriInfo uriInfo) {
342 MultivaluedMap<String,String> queryParams = uriInfo.getQueryParameters();
343 String omittedItemAction = queryParams.getFirst(VocabularyClient.OMITTED_ITEM_ACTION_QP);
344 return omittedItemAction;
348 * Returns the set of short identifiers in the abstract common list of authority items
350 private Set<String> getListOfShortIds(AbstractCommonList itemsList) {
351 HashSet<String> result = new HashSet<String>();
353 for (ListItem item : itemsList.getListItem()) {
354 String shortId = getShortId(item);
355 if (Tools.isEmpty(shortId) == false) {
363 private void createWithItemsPayload(
364 AbstractCommonList itemsList,
365 ServiceContext<PoxPayloadIn,
366 PoxPayloadOut> existingCtx,
367 String parentIdentifier,
368 ResourceMap resourceMap,
370 PoxPayloadIn input) throws Exception {
372 for (ListItem item : itemsList.getListItem()) {
373 String errMsg = null;
374 boolean success = true;
375 Response response = null;
376 PoxPayloadIn itemXmlPayload = getItemXmlPayload(item);
378 CoreSessionInterface repoSession = (CoreSessionInterface) existingCtx.getCurrentRepositorySession();
379 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
380 if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
382 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
385 // Throw an exception as soon as we have problems with any item
387 if (success == false) {
388 throw new DocumentException(errMsg);
393 private boolean handleItemsPayload(
395 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
396 String parentIdentifier,
397 ResourceMap resourceMap,
399 PoxPayloadIn input) throws Exception {
400 boolean result = false;
402 PayloadInputPart abstractCommonListPart = input.getPart(PoxPayload.ABSTRACT_COMMON_LIST_ROOT_ELEMENT_LABEL);
403 if (abstractCommonListPart != null) {
404 AbstractCommonList itemsList = (AbstractCommonList) abstractCommonListPart.getBody();
407 createWithItemsPayload(itemsList, existingCtx, parentIdentifier, resourceMap, uriInfo, input);
410 updateWithItemsPayload(itemsList, existingCtx, parentIdentifier, resourceMap, uriInfo, input);
413 result = true; // mark that we've handled an items-list payload
419 private String getFieldValue(ListItem item, String lookingFor) {
420 String result = null;
422 for (Element ele : item.getAny()) {
423 String fieldName = ele.getTagName();
424 String fieldValue = ele.getTextContent();
425 if (fieldName.equalsIgnoreCase(lookingFor)) {
434 public String getCsid(ListItem item) {
435 return getFieldValue(item, "csid");
438 private String getShortId(ListItem item) {
439 return getFieldValue(item, "shortIdentifier");
442 private String getDisplayName(ListItem item) {
443 return getFieldValue(item, "displayName");
447 * We'll return null if we can create a specifier from the list item.
452 private String getSpecifier(ListItem item) {
453 String result = null;
455 String csid = result = getCsid(item);
457 String shortId = getShortId(item);
458 if (shortId != null) {
459 result = Specifier.createShortIdURNValue(shortId);
467 * This is very brittle. If the class VocabularyitemsCommon changed with new fields we'd have to
468 * update this method.
472 * @throws DocumentException
474 private PoxPayloadIn getItemXmlPayload(ListItem item) throws DocumentException {
475 PoxPayloadIn result = null;
477 VocabularyitemsCommon vocabularyItem = new VocabularyitemsCommon();
478 for (Element ele : item.getAny()) {
479 String fieldName = ele.getTagName();
480 String fieldValue = ele.getTextContent();
483 vocabularyItem.setDisplayName(fieldValue);
486 case "shortIdentifier":
487 vocabularyItem.setShortIdentifier(fieldValue);
491 vocabularyItem.setOrder(fieldValue);
495 vocabularyItem.setSource(fieldValue);
499 vocabularyItem.setSourcePage(fieldValue);
503 vocabularyItem.setDescription(fieldValue);
507 vocabularyItem.setCsid(fieldValue);
511 vocabularyItem.setTermStatus(fieldValue);
515 // ignore other fields
520 // We need to create a short ID if one wasn't supplied
522 if (Tools.isEmpty(vocabularyItem.getShortIdentifier())) {
523 vocabularyItem.setShortIdentifier(AuthorityIdentifierUtils.generateShortIdentifierFromDisplayName(
524 vocabularyItem.getDisplayName() , null)); ;
527 result = new PoxPayloadIn(VocabularyClient.SERVICE_ITEM_PAYLOAD_NAME, vocabularyItem,
528 VOCABULARYITEMS_COMMON);
533 private Response createAuthorityItem(
534 CoreSessionInterface repoSession,
535 ResourceMap resourceMap,
537 String parentIdentifier, // Either a CSID or a URN form -e.g., a8ad38ec-1d7d-4bf2-bd31 or urn:cspace:name(bugsbunny)
538 PoxPayloadIn input) throws Exception {
539 Response result = null;
541 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(getItemServiceName(), input, resourceMap, uriInfo);
542 ctx.setCurrentRepositorySession(repoSession);
544 result = createAuthorityItem(ctx, parentIdentifier, AuthorityServiceUtils.UPDATE_REV,
545 AuthorityServiceUtils.PROPOSED, AuthorityServiceUtils.NOT_SAS_ITEM);
550 private PoxPayloadOut updateAuthorityItem(
551 CoreSessionInterface repoSession,
552 ResourceMap resourceMap,
554 String parentSpecifier, // Either a CSID or a URN form -e.g., a8ad38ec-1d7d-4bf2-bd31 or urn:cspace:name(bugsbunny)
555 String itemSpecifier, // Either a CSID or a URN form.
556 PoxPayloadIn theUpdate) throws Exception {
557 PoxPayloadOut result = null;
559 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(getItemServiceName(), theUpdate, resourceMap, uriInfo);
560 ctx.setCurrentRepositorySession(repoSession);
562 result = updateAuthorityItem(ctx, resourceMap, uriInfo, parentSpecifier, itemSpecifier, theUpdate,
563 AuthorityServiceUtils.UPDATE_REV, // passing TRUE so rev num increases, passing
564 AuthorityServiceUtils.NO_CHANGE, // don't change the state of the "proposed" field -we could be performing a sync or just a plain update
565 AuthorityServiceUtils.NO_CHANGE); // don't change the state of the "sas" field -we could be performing a sync or just a plain update
574 @Context Request request,
575 @Context ResourceMap resourceMap,
576 @Context UriInfo uriInfo,
577 @PathParam("csid") String specifier) {
578 Response result = null;
579 uriInfo = new UriInfoWrapper(uriInfo);
582 MultivaluedMap<String,String> queryParams = uriInfo.getQueryParameters();
583 String showItemsValue = (String)queryParams.getFirst(VocabularyClient.SHOW_ITEMS_QP);
584 boolean showItems = Tools.isTrue(showItemsValue);
585 if (showItems == true) {
587 // We'll honor paging params if we find any; otherwise we'll set the page size to 0 to get ALL the items
589 if (queryParams.containsKey(IClientQueryParams.PAGE_SIZE_PARAM) == false) {
590 queryParams.add(IClientQueryParams.PAGE_SIZE_PARAM, "0");
593 if (RefNameUtils.isTermRefname(specifier)) {
594 AuthorityTermInfo authorityTermInfo = RefNameUtils.parseAuthorityTermInfo(specifier);
595 String parentIdentifier = Specifier.createShortIdURNValue(authorityTermInfo.inAuthority.name);
596 String itemIdentifier = Specifier.createShortIdURNValue(authorityTermInfo.name);
597 result = this.getAuthorityItemResponse(request, uriInfo, resourceMap, parentIdentifier, itemIdentifier);
599 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(request, uriInfo);
600 PoxPayloadOut payloadout = getAuthority(ctx, request, uriInfo, specifier, showItems);
601 result = buildResponse(ctx, payloadout);
603 } catch (Exception e) {
604 throw bigReThrow(e, ServiceMessages.GET_FAILED, specifier);
607 if (result == null) {
608 Response response = Response.status(Response.Status.NOT_FOUND).entity(
609 "GET request failed. The requested Authority specifier:" + specifier + ": was not found.").type(
610 "text/plain").build();
611 throw new CSWebApplicationException(response);
618 public String getServiceName() {
619 return vocabularyServiceName;
623 public String getItemServiceName() {
624 return vocabularyItemServiceName;
628 public Class<VocabulariesCommon> getCommonPartClass() {
629 return VocabulariesCommon.class;
633 * @return the name of the property used to specify references for items in this type of
634 * authority. For most authorities, it is ServiceBindingUtils.AUTH_REF_PROP ("authRef").
635 * Some types (like Vocabulary) use a separate property.
638 protected String getRefPropName() {
639 return ServiceBindingUtils.TERM_REF_PROP;
643 protected String getOrderByField(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
644 String result = null;
646 result = ctx.getCommonPartLabel() + ":" + AuthorityItemJAXBSchema.DISPLAY_NAME;
652 protected String getPartialTermMatchField(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
653 return getOrderByField(ctx);
657 * The item schema for the Vocabulary service does not support a multi-valued term list. Only authorities that support
658 * term lists need to implement this method.
661 public String getItemTermInfoGroupXPathBase() {