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.common.vocabulary.nuxeo;
26 import java.util.List;
29 import javax.ws.rs.core.Response;
31 import org.collectionspace.services.client.AuthorityClient;
32 import org.collectionspace.services.client.CollectionSpaceClient;
33 import org.collectionspace.services.client.PayloadInputPart;
34 import org.collectionspace.services.client.VocabularyClient;
35 import org.collectionspace.services.client.PoxPayloadIn;
36 import org.collectionspace.services.client.PoxPayloadOut;
37 import org.collectionspace.services.common.ResourceMap;
38 import org.collectionspace.services.common.XmlTools;
39 import org.collectionspace.services.common.api.RefName;
40 import org.collectionspace.services.common.api.RefName.Authority;
41 import org.collectionspace.services.common.api.RefNameUtils;
42 import org.collectionspace.services.common.api.RefNameUtils.AuthorityInfo;
43 import org.collectionspace.services.common.api.RefNameUtils.AuthorityTermInfo;
44 import org.collectionspace.services.common.api.Tools;
45 import org.collectionspace.services.common.context.ServiceContext;
46 import org.collectionspace.services.common.document.DocumentException;
47 import org.collectionspace.services.common.document.DocumentHandler;
48 import org.collectionspace.services.common.document.DocumentNotFoundException;
49 import org.collectionspace.services.common.document.DocumentWrapper;
50 import org.collectionspace.services.common.vocabulary.AuthorityItemJAXBSchema;
51 import org.collectionspace.services.common.vocabulary.AuthorityJAXBSchema;
52 import org.collectionspace.services.common.vocabulary.AuthorityResource;
53 import org.collectionspace.services.common.vocabulary.AuthorityServiceUtils;
54 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthorityItemSpecifier;
55 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.Specifier;
56 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.SpecifierForm;
57 import org.collectionspace.services.config.service.ObjectPartType;
58 import org.collectionspace.services.lifecycle.TransitionDef;
59 import org.collectionspace.services.nuxeo.client.java.NuxeoDocumentModelHandler;
60 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
61 import org.collectionspace.services.nuxeo.client.java.RepositoryClientImpl;
62 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
63 import org.dom4j.Document;
64 import org.dom4j.Element;
65 import org.nuxeo.ecm.core.api.ClientException;
66 import org.nuxeo.ecm.core.api.DocumentModel;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
71 * AuthorityDocumentModelHandler
73 * $LastChangedRevision: $
76 public abstract class AuthorityDocumentModelHandler<AuthCommon>
77 extends NuxeoDocumentModelHandler<AuthCommon> {
79 private final Logger logger = LoggerFactory.getLogger(AuthorityDocumentModelHandler.class);
81 protected String authorityCommonSchemaName;
82 protected String authorityItemCommonSchemaName;
83 protected boolean shouldUpdateRevNumber = true; // default to updating the revision number
85 public AuthorityDocumentModelHandler(String authorityCommonSchemaName, String authorityItemCommonSchemaName) {
86 this.authorityCommonSchemaName = authorityCommonSchemaName;
87 this.authorityItemCommonSchemaName = authorityItemCommonSchemaName;
90 public void setShouldUpdateRevNumber(boolean flag) {
91 this.shouldUpdateRevNumber = flag;
94 public boolean getShouldUpdateRevNumber() {
95 return this.shouldUpdateRevNumber;
99 * The entity type expected from the JAX-RS Response object
101 public Class<String> getEntityResponseType() {
106 public void prepareSync() throws Exception {
107 this.setShouldUpdateRevNumber(AuthorityServiceUtils.DONT_UPDATE_REV); // Never update rev nums on sync operations
110 protected PayloadInputPart extractPart(Response res, String partLabel)
112 PoxPayloadIn input = new PoxPayloadIn((String)res.readEntity(getEntityResponseType()));
113 PayloadInputPart payloadInputPart = input.getPart(partLabel);
114 if (payloadInputPart == null) {
115 logger.error("Part " + partLabel + " was unexpectedly null.");
117 return payloadInputPart;
121 public boolean handleSync(DocumentWrapper<Object> wrapDoc) throws Exception {
122 boolean result = false;
123 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
124 Specifier specifier = (Specifier) wrapDoc.getWrappedObject();
126 // Get the rev number of the authority so we can compare with rev number of shared authority
128 DocumentModel docModel = NuxeoUtils.getDocFromSpecifier(ctx, getRepositorySession(), authorityCommonSchemaName, specifier);
129 Long localRev = (Long) NuxeoUtils.getProperyValue(docModel, AuthorityJAXBSchema.REV);
130 String shortId = (String) NuxeoUtils.getProperyValue(docModel, AuthorityJAXBSchema.SHORT_IDENTIFIER);
132 // Using the short ID of the local authority, create a URN specifier to retrieve the SAS authority
134 Specifier sasSpecifier = new Specifier(SpecifierForm.URN_NAME, RefNameUtils.createShortIdRefName(shortId));
135 PoxPayloadIn sasPayloadIn = AuthorityServiceUtils.requestPayloadIn(ctx, sasSpecifier, getEntityResponseType());
137 // If the authority on the SAS is newer, synch all the items and then the authority record as well
139 Long sasRev = getRevision(sasPayloadIn);
140 if (sasRev > localRev) {
142 // First, sync all the authority items
144 syncAllItems(ctx, sasSpecifier);
146 // Next, sync the authority resource/record itself
148 ResourceMap resourceMap = ctx.getResourceMap();
149 String resourceName = ctx.getClient().getServiceName();
150 AuthorityResource authorityResource = (AuthorityResource) resourceMap.get(resourceName);
151 PoxPayloadOut payloadOut = authorityResource.update(ctx, resourceMap, ctx.getUriInfo(), docModel.getName(),
153 if (payloadOut != null) {
154 ctx.setOutput(payloadOut);
163 * Get the list of authority items from the remote shared authority server (SAS) and try
164 * to synchronize them with the local items. If items exist on the remote but not the local, we'll create them.
166 protected int syncAllItems(ServiceContext ctx, Specifier sasSpecifier) throws Exception {
170 int alreadySynched = 0;
171 int totalItemsProcessed = 0;
173 // Iterate over the list of items/terms in the remote authority
175 PoxPayloadIn sasPayloadInItemList = getPayloadInItemList(ctx, sasSpecifier);
176 List<Element> itemList = getItemList(sasPayloadInItemList);
177 if (itemList != null) {
178 for (Element e:itemList) {
179 String remoteRefName = XmlTools.getElementValue(e, "refName");
180 long status = syncRemoteItemWithLocalItem(ctx, remoteRefName);
183 } else if (status == 0) {
188 totalItemsProcessed++;
192 logger.info(String.format("Total number of items processed during sync: %d", totalItemsProcessed));
193 logger.info(String.format("Number of items synchronized: %d", synched));
194 logger.info(String.format("Number of items created during sync: %d", created));
195 logger.info(String.format("Number not needing synchronization: %d", alreadySynched));
201 * This is a sync method.
203 * @param parentIdentifier - Must be in short-id-refname form -i.e., urn:cspace:name(shortid)
204 * @param itemIdentifier - Must be in short-id-refname form -i.e., urn:cspace:name(shortid)
207 protected void createLocalItem(ServiceContext ctx, String parentIdentifier, String itemIdentifier) throws Exception {
209 // Create a URN short ID specifier for the getting a copy of the remote authority item
211 Specifier authoritySpecifier = new Specifier(SpecifierForm.URN_NAME, parentIdentifier);
212 Specifier itemSpecifier = new Specifier(SpecifierForm.URN_NAME, itemIdentifier);
213 AuthorityItemSpecifier sasAuthorityItemSpecifier = new AuthorityItemSpecifier(authoritySpecifier, itemSpecifier);
215 // Get the remote payload
217 PoxPayloadIn sasPayloadIn = AuthorityServiceUtils.requestPayloadIn(sasAuthorityItemSpecifier,
218 ctx.getServiceName(), getEntityResponseType());
220 // Using the payload from the remote server, create a local copy of the item
222 ResourceMap resourceMap = ctx.getResourceMap();
223 String resourceName = ctx.getClient().getServiceName();
224 AuthorityResource authorityResource = (AuthorityResource) resourceMap.get(resourceName);
225 Response response = authorityResource.createAuthorityItemWithParentContext(ctx, authoritySpecifier.value,
226 sasPayloadIn, AuthorityServiceUtils.DONT_UPDATE_REV);
228 // Check the response for successful POST result
230 if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
231 throw new DocumentException(String.format("Could not create new authority item '%s' during synchronization of the '%s' authority.",
232 itemIdentifier, parentIdentifier));
234 // Handle the workflow state
240 * Try to synchronize a remote item (using its refName) with a local item. If the local doesn't yet
241 * exist, we'll create it.
243 * -1 = sync not needed; i.e., already in sync
245 * 1 = local item was missing so we created it
251 protected long syncRemoteItemWithLocalItem(ServiceContext ctx, String remoteRefName) throws Exception {
254 // Using the remote refname, create specifiers that we'll use to find the local versions
256 AuthorityTermInfo authorityTermInfo = RefNameUtils.parseAuthorityTermInfo(remoteRefName);
257 String parentIdentifier = RefNameUtils.createShortIdRefName(authorityTermInfo.inAuthority.name);
258 String itemIdentifier = RefNameUtils.createShortIdRefName(authorityTermInfo.name);
260 // We'll use the Authority JAX-RS resource to peform sync operations (creates and updates)
262 ResourceMap resourceMap = ctx.getResourceMap();
263 String resourceName = ctx.getClient().getServiceName();
264 AuthorityResource authorityResource = (AuthorityResource) resourceMap.get(resourceName);
266 PoxPayloadOut localItemPayloadOut;
268 localItemPayloadOut = authorityResource.getAuthorityItemWithParentContext(ctx, parentIdentifier, itemIdentifier);
269 } catch (DocumentNotFoundException dnf) {
271 // Document not found, means we need to create an item/term that exists only on the SAS
273 logger.info(String.format("Remote item with refname='%s' doesn't exist locally, so we'll create it.", remoteRefName));
274 createLocalItem(ctx, parentIdentifier, itemIdentifier);
275 return 1; // exit with status of 1 means we created a new authority item
278 // If we get here, we know the item exists both locally and remotely, so we need to synchronize them
280 PoxPayloadOut theUpdate = authorityResource.synchronizeItemWithParentContext(ctx, parentIdentifier, itemIdentifier);
281 if (theUpdate != null) {
282 result = 0; // mean we neeed to sync this item with SAS
283 logger.debug(String.format("Sync'd authority item parent='%s' id='%s with SAS. Updated payload is: \n%s",
284 parentIdentifier, itemIdentifier, theUpdate.getXmlPayload()));
287 return result; // -1 = no sync needed, 0 = sync'd, 1 = created new item
290 private PoxPayloadIn getPayloadInItemList(ServiceContext ctx, Specifier specifier) throws Exception {
291 PoxPayloadIn result = null;
293 AuthorityClient client = (AuthorityClient) ctx.getClient();
294 Response res = client.readItemList(specifier.value,
295 null, // partial term string
296 null // keyword string
299 int statusCode = res.getStatus();
301 // Check the status code of the response: does it match
302 // the expected response(s)?
303 if (logger.isDebugEnabled()) {
304 logger.debug(client.getClass().getCanonicalName() + ": status = " + statusCode);
307 result = new PoxPayloadIn((String)res.readEntity(getEntityResponseType())); // Get the entire response!
317 * Non standard injection of CSID into common part, since caller may access through
318 * shortId, and not know the CSID.
319 * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#extractPart(org.nuxeo.ecm.core.api.DocumentModel, java.lang.String, org.collectionspace.services.common.service.ObjectPartType)
322 protected Map<String, Object> extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta)
324 Map<String, Object> unQObjectProperties = super.extractPart(docModel, schema, partMeta);
326 // Add the CSID to the common part
327 if (partMeta.getLabel().equalsIgnoreCase(authorityCommonSchemaName)) {
328 String csid = getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
329 unQObjectProperties.put("csid", csid);
332 return unQObjectProperties;
335 public void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception {
336 super.fillAllParts(wrapDoc, action);
338 // Update the record's revision number on both CREATE and UPDATE actions, but not on SYNC
340 if (this.getShouldUpdateRevNumber() == true) { // We won't update rev numbers on synchronization with SAS
341 updateRevNumbers(wrapDoc);
345 protected void updateRevNumbers(DocumentWrapper<DocumentModel> wrapDoc) {
346 DocumentModel documentModel = wrapDoc.getWrappedObject();
347 Long rev = (Long)documentModel.getProperty(authorityCommonSchemaName, AuthorityJAXBSchema.REV);
353 documentModel.setProperty(authorityCommonSchemaName, AuthorityJAXBSchema.REV, rev);
357 * We consider workflow state changes as changes that should bump the revision number
359 * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#handleWorkflowTransition(org.collectionspace.services.common.document.DocumentWrapper, org.collectionspace.services.lifecycle.TransitionDef)
362 public void handleWorkflowTransition(DocumentWrapper<DocumentModel> wrapDoc, TransitionDef transitionDef) throws Exception {
363 // Update the revision number
364 updateRevNumbers(wrapDoc);
368 public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
369 super.handleCreate(wrapDoc);
371 // Uncomment once debugged and App layer is read to integrate
372 // Experimenting with this uncommented now ...
373 handleDisplayNameAsShortIdentifier(wrapDoc.getWrappedObject(), authorityCommonSchemaName);
374 updateRefnameForAuthority(wrapDoc, authorityCommonSchemaName);//CSPACE-3178
377 protected String buildWhereForShortId(String name) {
378 return authorityCommonSchemaName
379 + ":" + AuthorityJAXBSchema.SHORT_IDENTIFIER
383 private boolean isUnique(DocumentModel docModel, String schemaName) throws DocumentException {
387 private boolean temp_isUnique(DocumentModel docModel, String schemaName) throws DocumentException {
388 boolean result = true;
390 ServiceContext ctx = this.getServiceContext();
391 String shortIdentifier = (String) docModel.getProperty(schemaName, AuthorityJAXBSchema.SHORT_IDENTIFIER);
392 String nxqlWhereClause = buildWhereForShortId(shortIdentifier);
394 DocumentWrapper<DocumentModel> searchResultWrapper = getRepositoryClient(ctx).findDoc(ctx, nxqlWhereClause);
395 if (searchResultWrapper != null) {
397 if (logger.isInfoEnabled() == true) {
398 DocumentModel searchResult = searchResultWrapper.getWrappedObject();
399 String debugMsg = String.format("Could not create a new authority with a short identifier of '%s', because one already exists with the same short identifer: CSID = '%s'",
400 shortIdentifier, searchResult.getName());
401 logger.trace(debugMsg);
404 } catch (DocumentNotFoundException e) {
405 // Not a problem, just means we couldn't find another authority with that short ID
412 * If no short identifier was provided in the input payload,
413 * generate a short identifier from the display name. Either way though,
414 * the short identifier needs to be unique.
416 private void handleDisplayNameAsShortIdentifier(DocumentModel docModel, String schemaName) throws Exception {
417 String shortIdentifier = (String) docModel.getProperty(schemaName, AuthorityJAXBSchema.SHORT_IDENTIFIER);
418 String displayName = (String) docModel.getProperty(schemaName, AuthorityJAXBSchema.DISPLAY_NAME);
419 String shortDisplayName = "";
420 String generateShortIdentifier = null;
421 if (Tools.isEmpty(shortIdentifier)) {
422 generateShortIdentifier = AuthorityIdentifierUtils.generateShortIdentifierFromDisplayName(displayName, shortDisplayName);
423 docModel.setProperty(schemaName, AuthorityJAXBSchema.SHORT_IDENTIFIER, shortIdentifier);
426 if (isUnique(docModel, schemaName) == false) {
427 String shortId = generateShortIdentifier == null ? shortIdentifier : generateShortIdentifier;
428 String errMsgVerb = generateShortIdentifier == null ? "supplied" : "generated";
429 String errMsg = String.format("The %s short identifier '%s' was not unique, so the new authority could not be created.",
430 errMsgVerb, shortId);
431 throw new DocumentException(errMsg);
436 * Generate a refName for the authority from the short identifier
439 * All refNames for authorities are generated. If a client supplies
440 * a refName, it will be overwritten during create (per this method)
441 * or discarded during update (per filterReadOnlyPropertiesForPart).
443 * @see #filterReadOnlyPropertiesForPart(Map<String, Object>, org.collectionspace.services.common.service.ObjectPartType)
446 protected void updateRefnameForAuthority(DocumentWrapper<DocumentModel> wrapDoc, String schemaName) throws Exception {
447 DocumentModel docModel = wrapDoc.getWrappedObject();
448 RefName.Authority authority = (Authority) getRefName(getServiceContext(), docModel);
449 String refName = authority.toString();
450 docModel.setProperty(schemaName, AuthorityJAXBSchema.REF_NAME, refName);
454 public RefName.RefNameInterface getRefName(ServiceContext ctx,
455 DocumentModel docModel) {
456 RefName.RefNameInterface refname = null;
459 String shortIdentifier = (String) docModel.getProperty(authorityCommonSchemaName, AuthorityJAXBSchema.SHORT_IDENTIFIER);
460 String displayName = (String) docModel.getProperty(authorityCommonSchemaName, AuthorityJAXBSchema.DISPLAY_NAME);
461 RefName.Authority authority = RefName.Authority.buildAuthority(ctx.getTenantName(),
462 ctx.getServiceName(),
463 null, // Only use shortId form!!!
467 } catch (Exception e) {
468 logger.error(e.getMessage(), e);
475 protected String getRefnameDisplayName(DocumentWrapper<DocumentModel> docWrapper) {
476 String result = null;
478 DocumentModel docModel = docWrapper.getWrappedObject();
479 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = this.getServiceContext();
480 RefName.Authority refname = (RefName.Authority)getRefName(ctx, docModel);
481 result = refname.getDisplayName();
486 public String getShortIdentifier(ServiceContext ctx, String authCSID, String schemaName) throws Exception {
487 String shortIdentifier = null;
488 CoreSessionInterface repoSession = null;
489 boolean releaseSession = false;
491 RepositoryClientImpl nuxeoRepoClient = (RepositoryClientImpl)this.getRepositoryClient(ctx);
493 repoSession = nuxeoRepoClient.getRepositorySession(ctx);
494 DocumentWrapper<DocumentModel> wrapDoc = nuxeoRepoClient.getDocFromCsid(ctx, repoSession, authCSID);
495 DocumentModel docModel = wrapDoc.getWrappedObject();
496 if (docModel == null) {
497 throw new DocumentNotFoundException(String.format("Could not find authority resource with CSID='%s'.", authCSID));
499 shortIdentifier = (String) docModel.getProperty(schemaName, AuthorityJAXBSchema.SHORT_IDENTIFIER);
500 } catch (ClientException ce) {
501 throw new RuntimeException("AuthorityDocHandler Internal Error: cannot get shortId!", ce);
503 if (repoSession != null) {
504 nuxeoRepoClient.releaseRepositorySession(ctx, repoSession);
508 return shortIdentifier;
512 * Filters out selected values supplied in an update request.
514 * @param objectProps the properties filtered out from the update payload
515 * @param partMeta metadata for the object to fill
518 public void filterReadOnlyPropertiesForPart(
519 Map<String, Object> objectProps, ObjectPartType partMeta) {
520 super.filterReadOnlyPropertiesForPart(objectProps, partMeta);
521 String commonPartLabel = getServiceContext().getCommonPartLabel();
522 if (partMeta.getLabel().equalsIgnoreCase(commonPartLabel)) {
523 objectProps.remove(AuthorityJAXBSchema.CSID);
524 objectProps.remove(AuthorityJAXBSchema.SHORT_IDENTIFIER);
525 objectProps.remove(AuthorityJAXBSchema.REF_NAME);