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.document.JaxbUtils;
44 import org.collectionspace.services.common.repository.RepositoryClient;
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.jaxb.AbstractCommonList;
51 import org.collectionspace.services.jaxb.AbstractCommonList.ListItem;
52 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
53 import org.collectionspace.services.nuxeo.client.java.NuxeoRepositoryClientImpl;
54 import org.collectionspace.services.vocabulary.nuxeo.VocabularyItemDocumentModelHandler;
55 import org.collectionspace.services.workflow.WorkflowCommon;
56 import org.nuxeo.ecm.core.api.DocumentModel;
57 import org.nuxeo.ecm.core.api.DocumentModelList;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60 import org.w3c.dom.Element;
62 import java.util.HashSet;
63 import java.util.List;
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;
78 @Path("/" + VocabularyClient.SERVICE_PATH_COMPONENT)
79 public class VocabularyResource extends
80 AuthorityResource<VocabulariesCommon, VocabularyItemDocumentModelHandler> {
86 private final static String vocabularyServiceName = VocabularyClient.SERVICE_PATH_COMPONENT;
88 private final static String VOCABULARIES_COMMON = "vocabularies_common";
90 private final static String vocabularyItemServiceName = "vocabularyitems";
91 private final static String VOCABULARYITEMS_COMMON = "vocabularyitems_common";
93 final Logger logger = LoggerFactory.getLogger(VocabularyResource.class);
95 public VocabularyResource() {
96 super(VocabulariesCommon.class, VocabularyResource.class,
97 VOCABULARIES_COMMON, VOCABULARYITEMS_COMMON);
102 public Response createAuthority(
103 @Context ResourceMap resourceMap,
104 @Context UriInfo uriInfo,
107 // 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
108 // transaction code to deal with a database level UNIQUE constraint violations on the 'shortidentifier' column of the vocabularies_common table.
109 // Therefore, to prevent having multiple authorities with the same shortid, we need to synchronize
110 // the code that creates new authorities. The authority document model handler will first check for authorities with the same short id before
111 // trying to create a new authority.
113 synchronized(AuthorityResource.class) {
115 PoxPayloadIn input = new PoxPayloadIn(xmlPayload);
116 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(input);
117 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = this.getRepositoryClient(ctx);
119 CoreSessionInterface repoSession = repoClient.getRepositorySession(ctx);
121 DocumentHandler<?, AbstractCommonList, DocumentModel, DocumentModelList> handler = createDocumentHandler(ctx);
122 String csid = repoClient.create(ctx, handler);
124 // Handle any supplied list of items/terms
126 handleItemsPayload(Method.POST, ctx, csid, resourceMap, uriInfo, input);
127 UriBuilder path = UriBuilder.fromResource(resourceClass);
128 path.path("" + csid);
129 Response response = Response.created(path.build()).build();
131 } catch (Throwable t) {
132 repoSession.setTransactionRollbackOnly();
135 repoClient.releaseRepositorySession(ctx, repoSession);
137 } catch (Exception e) {
138 throw bigReThrow(e, ServiceMessages.CREATE_FAILED);
146 public byte[] updateAuthority(
147 @Context ResourceMap resourceMap,
149 @PathParam("csid") String specifier,
151 PoxPayloadOut result = null;
153 UriInfoWrapper uriInfo = new UriInfoWrapper(ui); // We need to make the queryParams maps read-write instead of read-only
154 PoxPayloadIn theUpdate = new PoxPayloadIn(xmlPayload);
155 Specifier spec = Specifier.getSpecifier(specifier, "updateAuthority", "UPDATE");
156 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(theUpdate, uriInfo);
157 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient = this.getRepositoryClient(ctx);
159 CoreSessionInterface repoSession = repoClient.getRepositorySession(ctx);
161 DocumentHandler<?, AbstractCommonList, DocumentModel, DocumentModelList> handler = createDocumentHandler(ctx);
163 if (spec.form == SpecifierForm.CSID) {
166 String whereClause = RefNameServiceUtils.buildWhereForAuthByName(authorityCommonSchemaName, spec.value);
167 csid = getRepositoryClient(ctx).findDocCSID(null, ctx, whereClause);
169 getRepositoryClient(ctx).update(ctx, csid, handler);
170 handleItemsPayload(Method.PUT, ctx, csid, resourceMap, uriInfo, theUpdate);
171 result = ctx.getOutput();
172 } catch (Throwable t) {
173 repoSession.setTransactionRollbackOnly();
176 repoClient.releaseRepositorySession(ctx, repoSession);
178 } catch (Exception e) {
179 throw bigReThrow(e, ServiceMessages.UPDATE_FAILED);
181 return result.getBytes();
184 private void updateWithItemsPayload(
185 AbstractCommonList itemsList,
186 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
187 String parentIdentifier,
188 ResourceMap resourceMap,
190 PoxPayloadIn input) throws Exception {
192 CoreSessionInterface repoSession = (CoreSessionInterface) existingCtx.getCurrentRepositorySession();
194 // First try to update and/or create items in the incoming payload
196 for (ListItem item : itemsList.getListItem()) {
197 String errMsg = null;
198 boolean success = true;
199 Response response = null;
200 PoxPayloadOut payloadOut = null;
201 PoxPayloadIn itemXmlPayload = getItemXmlPayload(item);
202 String itemSpecifier = getSpecifier(item);
203 if (itemSpecifier != null) {
205 payloadOut = updateAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemSpecifier, itemXmlPayload);
206 if (payloadOut == null) {
208 errMsg = String.format("Could not update the term list payload of vocabuary '%s'.", parentIdentifier);
210 } catch (DocumentNotFoundException dnf) {
212 // Since the item doesn't exist, we're being ask to create it
214 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
215 if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
217 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
222 errMsg = String.format("Could not update the term list payload of vocabuary '%s' because one of the item is missing a CSID or short identifier value.",
226 // Throw an exception as soon as we have problems with any item
228 if (success == false) {
229 throw new DocumentException(errMsg);
234 // Next, delete the items that were omitted from the incoming payload
236 if (shouldDeleteOmittedItems(uriInfo) == true) {
237 String omittedItemAction = getOmittedItemAction(uriInfo);
238 Set<String> shortIdsInPayload = getListOfShortIds(itemsList);
239 AbstractCommonList abstractCommonList = this.getAuthorityItemList(existingCtx, parentIdentifier, uriInfo);
240 if (abstractCommonList != null && !Tools.isEmpty(abstractCommonList.getListItem())) {
241 if (omittedItemAction.equalsIgnoreCase(VocabularyClient.DELETE_OMITTED_ITEMS)) {
242 deleteAuthorityItems(existingCtx, abstractCommonList, shortIdsInPayload, parentIdentifier);
244 sotfDeleteAuthorityItems(existingCtx, abstractCommonList, shortIdsInPayload, parentIdentifier);
250 private void deleteAuthorityItems(
251 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
252 AbstractCommonList abstractCommonList,
253 Set<String> shortIdsInPayload,
254 String parentIdentifier) throws Exception {
256 for (ListItem item : abstractCommonList.getListItem()) {
257 String shortId = getShortId(item);
258 if (shortIdsInPayload.contains(shortId) == false) {
259 deleteAuthorityItem(existingCtx, parentIdentifier, getCsid(item), AuthorityServiceUtils.UPDATE_REV);
264 private void sotfDeleteAuthorityItems(
265 ServiceContext<PoxPayloadIn, PoxPayloadOut> existingCtx,
266 AbstractCommonList abstractCommonList,
267 Set<String> shortIdsInPayload,
268 String parentIdentifier) throws Exception {
270 for (ListItem item : abstractCommonList.getListItem()) {
271 String shortId = getShortId(item);
272 if (shortIdsInPayload.contains(shortId) == false) {
273 //deleteAuthorityItem(existingCtx, parentIdentifier, getCsid(item), AuthorityServiceUtils.UPDATE_REV);
274 this.updateItemWorkflowWithTransition(existingCtx, parentIdentifier, getCsid(item),
275 WorkflowClient.WORKFLOWTRANSITION_DELETE, AuthorityServiceUtils.UPDATE_REV);
280 private boolean shouldDeleteOmittedItems(UriInfo uriInfo) throws DocumentException {
281 boolean result = false;
283 String omittedItemAction = getOmittedItemAction(uriInfo);
284 if (Tools.isEmpty(omittedItemAction) == false) {
285 switch (omittedItemAction) {
286 case VocabularyClient.DELETE_OMITTED_ITEMS:
287 case VocabularyClient.SOFTDELETE_OMITTED_ITEMS:
290 case VocabularyClient.IGNORE_OMITTED_ITEMS:
294 String msg = String.format("Unknown value '%s' for update on a vocabulary/termlist resource.", omittedItemAction);
295 throw new DocumentException(msg);
302 private String getOmittedItemAction(UriInfo uriInfo) {
303 MultivaluedMap<String,String> queryParams = uriInfo.getQueryParameters();
304 String omittedItemAction = queryParams.getFirst(VocabularyClient.OMITTED_ITEM_ACTION_QP);
305 return omittedItemAction;
309 * Returns the set of short identifiers in the abstract common list of authority items
311 private Set<String> getListOfShortIds(AbstractCommonList itemsList) {
312 HashSet<String> result = new HashSet<String>();
314 for (ListItem item : itemsList.getListItem()) {
315 result.add(getShortId(item));
321 private void createWithItemsPayload(
322 AbstractCommonList itemsList,
323 ServiceContext existingCtx,
324 String parentIdentifier,
325 ResourceMap resourceMap,
327 PoxPayloadIn input) throws Exception {
329 for (ListItem item : itemsList.getListItem()) {
330 String errMsg = null;
331 boolean success = true;
332 Response response = null;
333 PoxPayloadIn itemXmlPayload = getItemXmlPayload(item);
335 CoreSessionInterface repoSession = (CoreSessionInterface) existingCtx.getCurrentRepositorySession();
336 response = this.createAuthorityItem(repoSession, resourceMap, uriInfo, parentIdentifier, itemXmlPayload);
337 if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
339 errMsg = String.format("Could not create the term list payload of vocabuary '%s'.", parentIdentifier);
342 // Throw an exception as soon as we have problems with any item
344 if (success == false) {
345 throw new DocumentException(errMsg);
350 private boolean handleItemsPayload(
352 ServiceContext existingCtx,
353 String parentIdentifier,
354 ResourceMap resourceMap,
356 PoxPayloadIn input) throws Exception {
357 boolean result = true;
359 PayloadInputPart abstractCommonListPart = input.getPart(PoxPayload.ABSTRACT_COMMON_LIST_ROOT_ELEMENT_LABEL);
360 if (abstractCommonListPart != null) {
361 AbstractCommonList itemsList = (AbstractCommonList) abstractCommonListPart.getBody();
364 createWithItemsPayload(itemsList, existingCtx, parentIdentifier, resourceMap, uriInfo, input);
367 updateWithItemsPayload(itemsList, existingCtx, parentIdentifier, resourceMap, uriInfo, input);
375 private String getCsid(ListItem item) {
376 String result = null;
378 for (Element ele : item.getAny()) {
379 String fieldName = ele.getTagName();
380 String fieldValue = ele.getTextContent();
381 if (fieldName.equalsIgnoreCase("csid")) {
390 private String getShortId(ListItem item) {
391 String result = null;
393 for (Element ele : item.getAny()) {
394 String fieldName = ele.getTagName();
395 String fieldValue = ele.getTextContent();
396 if (fieldName.equalsIgnoreCase("shortIdentifier")) {
406 * We'll return null if we can create a specifier from the list item.
411 private String getSpecifier(ListItem item) {
412 String result = null;
414 String csid = getCsid(item);
416 String shortId = getShortId(item);
417 if (shortId != null) {
418 result = Specifier.createShortIdURNValue(shortId);
426 * This is very brittle. If the class VocabularyitemsCommon changed with new fields we'd have to
427 * update this method.
431 * @throws DocumentException
433 private PoxPayloadIn getItemXmlPayload(ListItem item) throws DocumentException {
434 PoxPayloadIn result = null;
436 VocabularyitemsCommon vocabularyItem = new VocabularyitemsCommon();
437 for (Element ele : item.getAny()) {
438 String fieldName = ele.getTagName();
439 String fieldValue = ele.getTextContent();
442 vocabularyItem.setDisplayName(fieldValue);
445 case "shortIdentifier":
446 vocabularyItem.setShortIdentifier(fieldValue);
450 vocabularyItem.setOrder(fieldValue);
454 vocabularyItem.setSource(fieldValue);
458 vocabularyItem.setSourcePage(fieldValue);
462 vocabularyItem.setDescription(fieldValue);
466 vocabularyItem.setCsid(fieldValue);
470 throw new DocumentException(String.format("Unknown field '%s' in vocabulary item payload.",
475 result = new PoxPayloadIn(VocabularyClient.SERVICE_ITEM_PAYLOAD_NAME, vocabularyItem,
476 VOCABULARYITEMS_COMMON);
481 private Response createAuthorityItem(
482 CoreSessionInterface repoSession,
483 ResourceMap resourceMap,
485 String parentIdentifier, // Either a CSID or a URN form -e.g., a8ad38ec-1d7d-4bf2-bd31 or urn:cspace:name(bugsbunny)
486 PoxPayloadIn input) throws Exception {
487 Response result = null;
489 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(getItemServiceName(), input, resourceMap, uriInfo);
490 ctx.setCurrentRepositorySession(repoSession);
492 result = createAuthorityItem(ctx, parentIdentifier, AuthorityServiceUtils.UPDATE_REV,
493 AuthorityServiceUtils.PROPOSED, AuthorityServiceUtils.NOT_SAS_ITEM);
498 private PoxPayloadOut updateAuthorityItem(
499 CoreSessionInterface repoSession,
500 ResourceMap resourceMap,
502 String parentSpecifier, // Either a CSID or a URN form -e.g., a8ad38ec-1d7d-4bf2-bd31 or urn:cspace:name(bugsbunny)
503 String itemSpecifier, // Either a CSID or a URN form.
504 PoxPayloadIn theUpdate) throws Exception {
505 PoxPayloadOut result = null;
507 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(getItemServiceName(), theUpdate, resourceMap, uriInfo);
508 ctx.setCurrentRepositorySession(repoSession);
510 result = updateAuthorityItem(ctx, resourceMap, uriInfo, parentSpecifier, itemSpecifier, theUpdate,
511 AuthorityServiceUtils.UPDATE_REV, // passing TRUE so rev num increases, passing
512 AuthorityServiceUtils.NO_CHANGE, // don't change the state of the "proposed" field -we could be performing a sync or just a plain update
513 AuthorityServiceUtils.NO_CHANGE); // don't change the state of the "sas" field -we could be performing a sync or just a plain update
522 @Context Request request,
523 @Context UriInfo uriInfo,
524 @PathParam("csid") String specifier) {
525 Response result = null;
526 uriInfo = new UriInfoWrapper(uriInfo);
529 MultivaluedMap<String,String> queryParams = uriInfo.getQueryParameters();
530 String showItemsValue = (String)queryParams.getFirst(VocabularyClient.SHOW_ITEMS_QP);
531 boolean showItems = Tools.isTrue(showItemsValue);
532 if (showItems == true) {
534 // We'll honor paging params if we find any; otherwise we'll set the page size to 0 to get ALL the items
536 if (queryParams.containsKey(IClientQueryParams.PAGE_SIZE_PARAM) == false) {
537 queryParams.add(IClientQueryParams.PAGE_SIZE_PARAM, "0");
541 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = createServiceContext(request, uriInfo);
542 PoxPayloadOut payloadout = getAuthority(ctx, request, uriInfo, specifier, showItems);
543 result = buildResponse(ctx, payloadout);
544 } catch (Exception e) {
545 throw bigReThrow(e, ServiceMessages.GET_FAILED, specifier);
548 if (result == null) {
549 Response response = Response.status(Response.Status.NOT_FOUND).entity(
550 "GET request failed. The requested Authority specifier:" + specifier + ": was not found.").type(
551 "text/plain").build();
552 throw new CSWebApplicationException(response);
559 public String getServiceName() {
560 return vocabularyServiceName;
564 public String getItemServiceName() {
565 return vocabularyItemServiceName;
569 public Class<VocabulariesCommon> getCommonPartClass() {
570 return VocabulariesCommon.class;
574 * @return the name of the property used to specify references for items in this type of
575 * authority. For most authorities, it is ServiceBindingUtils.AUTH_REF_PROP ("authRef").
576 * Some types (like Vocabulary) use a separate property.
579 protected String getRefPropName() {
580 return ServiceBindingUtils.TERM_REF_PROP;
584 protected String getOrderByField(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
585 String result = null;
587 result = authorityItemCommonSchemaName + ":" + VocabularyItemJAXBSchema.DISPLAY_NAME;
593 protected String getPartialTermMatchField(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
594 return getOrderByField(ctx);
598 * The item schema for the Vocabulary service does not support a multi-valued term list. Only authorities that support
599 * term lists need to implement this method.
602 public String getItemTermInfoGroupXPathBase() {