2 * This document is a part of the source code and related artifacts for
3 * CollectionSpace, an open source collections management system for museums and
4 * related institutions:
6 * http://www.collectionspace.org http://wiki.collectionspace.org
8 * Copyright 2009 University of California at Berkeley
10 * Licensed under the Educational Community License (ECL), Version 2.0. You may
11 * not use this file except in compliance with this License.
13 * You may obtain a copy of the ECL 2.0 License at
15 * https://source.collectionspace.org/collection-space/LICENSE.txt
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
19 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
20 * License for the specific language governing permissions and limitations under
23 package org.collectionspace.services.common.vocabulary;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.util.List;
31 import javax.ws.rs.core.Response;
33 import org.nuxeo.ecm.core.api.ClientException;
34 import org.nuxeo.ecm.core.api.DocumentModel;
35 import org.nuxeo.ecm.core.api.DocumentModelList;
36 import org.nuxeo.ecm.core.api.model.Property;
37 import org.nuxeo.ecm.core.api.model.PropertyException;
38 import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
39 import org.nuxeo.ecm.core.api.model.impl.primitives.StringProperty;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42 import org.collectionspace.services.client.CollectionSpaceClient;
43 import org.collectionspace.services.client.IQueryManager;
44 import org.collectionspace.services.client.IRelationsManager;
45 import org.collectionspace.services.client.PoxPayloadIn;
46 import org.collectionspace.services.client.PoxPayloadOut;
47 import org.collectionspace.services.common.CSWebApplicationException;
48 import org.collectionspace.services.common.ServiceMain;
49 import org.collectionspace.services.common.StoredValuesUriTemplate;
50 import org.collectionspace.services.common.UriTemplateFactory;
51 import org.collectionspace.services.common.UriTemplateRegistry;
52 import org.collectionspace.services.common.UriTemplateRegistryKey;
53 import org.collectionspace.services.common.context.ServiceContext;
54 import org.collectionspace.services.common.context.AbstractServiceContextImpl;
55 import org.collectionspace.services.common.api.RefNameUtils;
56 import org.collectionspace.services.common.api.Tools;
57 import org.collectionspace.services.common.api.RefNameUtils.AuthorityTermInfo;
58 import org.collectionspace.services.common.authorityref.AuthorityRefDocList;
59 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
60 import org.collectionspace.services.common.context.ServiceBindingUtils;
61 import org.collectionspace.services.common.document.DocumentException;
62 import org.collectionspace.services.common.document.DocumentFilter;
63 import org.collectionspace.services.common.document.DocumentNotFoundException;
64 import org.collectionspace.services.common.document.DocumentUtils;
65 import org.collectionspace.services.common.document.DocumentWrapper;
66 import org.collectionspace.services.common.query.QueryManager;
67 import org.collectionspace.services.common.relation.RelationUtils;
68 import org.collectionspace.services.common.repository.RepositoryClient;
69 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
70 import org.collectionspace.services.nuxeo.client.java.NuxeoDocumentModelHandler;
71 import org.collectionspace.services.nuxeo.client.java.NuxeoRepositoryClientImpl;
72 import org.collectionspace.services.common.security.SecurityUtils;
73 import org.collectionspace.services.config.service.ServiceBindingType;
74 import org.collectionspace.services.jaxb.AbstractCommonList;
75 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
78 * RefNameServiceUtils is a collection of services utilities related to refName
81 * $LastChangedRevision: $ $LastChangedDate: $
83 public class RefNameServiceUtils {
85 public static enum SpecifierForm {
86 CSID, URN_NAME // Either a CSID or a short ID
89 public static class Specifier {
91 // URN statics for things like urn:cspace:name(grover)
93 final static String URN_PREFIX = "urn:cspace:";
94 final static int URN_PREFIX_LEN = URN_PREFIX.length();
95 final static String URN_PREFIX_NAME = "name(";
96 final static int URN_NAME_PREFIX_LEN = URN_PREFIX_LEN + URN_PREFIX_NAME.length();
97 final static String URN_PREFIX_ID = "id(";
98 final static int URN_ID_PREFIX_LEN = URN_PREFIX_LEN + URN_PREFIX_ID.length();
100 public SpecifierForm form;
103 public Specifier(SpecifierForm form, String value) {
109 * identifier can be a CSID form like a8ad38ec-1d7d-4bf2-bd31 or a URN form like urn:cspace:name(shortid) or urn:cspace:id(a8ad38ec-1d7d-4bf2-bd31)
112 public static Specifier getSpecifier(String identifier) throws CSWebApplicationException {
113 return getSpecifier(identifier, "NO-OP", "NO-OP");
117 * identifier can be a CSID form like a8ad38ec-1d7d-4bf2-bd31 or a URN form like urn:cspace:name(shortid) or urn:cspace:id(a8ad38ec-1d7d-4bf2-bd31)
120 public static Specifier getSpecifier(String identifier, String method, String op) throws CSWebApplicationException {
121 Specifier result = null;
123 if (identifier != null) {
124 if (!identifier.startsWith(URN_PREFIX)) {
125 // We'll assume it is a CSID and complain if it does not match
126 result = new Specifier(SpecifierForm.CSID, identifier);
128 if (identifier.startsWith(URN_PREFIX_NAME, URN_PREFIX_LEN)) {
129 int closeParen = identifier.indexOf(')', URN_NAME_PREFIX_LEN);
130 if (closeParen >= 0) {
131 result = new Specifier(SpecifierForm.URN_NAME,
132 identifier.substring(URN_NAME_PREFIX_LEN, closeParen));
134 } else if (identifier.startsWith(URN_PREFIX_ID, URN_PREFIX_LEN)) {
135 int closeParen = identifier.indexOf(')', URN_ID_PREFIX_LEN);
136 if (closeParen >= 0) {
137 result = new Specifier(SpecifierForm.CSID,
138 identifier.substring(URN_ID_PREFIX_LEN, closeParen));
141 logger.error(method + ": bad or missing specifier!");
142 Response response = Response.status(Response.Status.BAD_REQUEST).entity(
143 op + " failed on bad or missing Authority specifier").type(
144 "text/plain").build();
145 throw new CSWebApplicationException(response);
154 * Creates a refName in the name / shortIdentifier form.
156 * @param shortId a shortIdentifier for an authority or one of its terms
157 * @return a refName for that authority or term, in the name / shortIdentifier form.
158 * If the provided shortIdentifier is null or empty, returns
161 public static String createShortIdURNValue(String shortId) {
162 String result = null;
164 if (shortId != null || !shortId.trim().isEmpty()) {
165 result = String.format("urn:cspace:name(%s)", shortId);
172 * Returns a URN string identifier -e.g., urn:cspace:name(patrick) or urn:cspace:id(579d18a6-b464-4b11-ba3a)
177 public String getURNValue() throws Exception {
178 String result = null;
180 if (form == SpecifierForm.CSID) {
181 result = String.format("urn:cspace:id(%s)", value);
182 } else if (form == SpecifierForm.URN_NAME) {
183 result = String.format("urn:cspace:name(%s)", value);
185 throw new Exception(String.format("Unknown specifier form '%s'.", form));
192 public static class AuthorityItemSpecifier {
193 private Specifier parentSpecifier;
194 private Specifier itemSpecifier;
196 public AuthorityItemSpecifier(Specifier parentSpecifier, Specifier itemSpecifier) {
197 this.parentSpecifier = parentSpecifier;
198 this.itemSpecifier = itemSpecifier;
201 public AuthorityItemSpecifier(SpecifierForm form, String parentCsidOrShortId, String itemCsidOrShortId) {
202 this.parentSpecifier = new Specifier(form, parentCsidOrShortId);
203 this.itemSpecifier = new Specifier(form, itemCsidOrShortId);
206 public Specifier getParentSpecifier() {
207 return this.parentSpecifier;
210 public Specifier getItemSpecifier() {
211 return this.itemSpecifier;
215 public String toString() {
216 String result = "%s/items/%s";
219 result = String.format(result, this.parentSpecifier.getURNValue(), this.itemSpecifier.getURNValue());
220 } catch (Exception e) {
221 result = "Unknown error trying to get string representation of Specifier.";
222 logger.error(result, e);
229 public static class AuthRefConfigInfo {
231 public String getQualifiedDisplayName() {
232 return (Tools.isBlank(schema))
233 ? displayName : DocumentUtils.appendSchemaName(schema, displayName);
236 public String getDisplayName() {
240 public void setDisplayName(String displayName) {
241 this.displayName = displayName;
246 public String getSchema() {
250 public void setSchema(String schema) {
251 this.schema = schema;
254 public String getFullPath() {
258 public void setFullPath(String fullPath) {
259 this.fullPath = fullPath;
262 protected String[] pathEls;
264 public AuthRefConfigInfo(AuthRefConfigInfo arci) {
265 this.displayName = arci.displayName;
266 this.schema = arci.schema;
267 this.fullPath = arci.fullPath;
268 this.pathEls = arci.pathEls;
269 // Skip the pathElse check, since we are creatign from another (presumably valid) arci.
272 public AuthRefConfigInfo(String displayName, String schema, String fullPath, String[] pathEls) {
273 this.displayName = displayName;
274 this.schema = schema;
275 this.fullPath = fullPath;
276 this.pathEls = pathEls;
280 // Split a config value string like "intakes_common:collector", or
281 // "collectionobjects_common:contentPeoples|contentPeople"
282 // "collectionobjects_common:assocEventGroupList/*/assocEventPlace"
283 // If has a pipe ('|') second part is a displayLabel, and first is path
284 // Otherwise, entry is a path, and can use the last pathElement as displayName
285 // Should be schema qualified.
286 public AuthRefConfigInfo(String configString) {
287 String[] pair = configString.split("\\|", 2);
289 String displayName, fullPath;
290 if (pair.length == 1) {
291 // no label specifier, so we'll defer getting label
293 pathEls = pair[0].split("/");
294 displayName = pathEls[pathEls.length - 1];
297 pathEls = pair[0].split("/");
298 displayName = pair[1];
300 String[] schemaSplit = pathEls[0].split(":", 2);
302 if (schemaSplit.length == 1) { // schema not specified
305 schema = schemaSplit[0];
306 if (pair.length == 1 && pathEls.length == 1) { // simplest case of field in top level schema, no labelll
307 displayName = schemaSplit[1]; // Have to fix up displayName to have no schema
310 this.displayName = displayName;
311 this.schema = schema;
312 this.fullPath = fullPath;
313 this.pathEls = pathEls;
317 protected void checkPathEls() {
318 int len = pathEls.length;
320 throw new InternalError("Bad values in authRef info - caller screwed up:" + fullPath);
322 // Handle case of them putting a leading slash on the path
323 if (len > 1 && pathEls[0].endsWith(":")) {
325 String[] newArray = new String[len];
326 newArray[0] = pathEls[0] + pathEls[1];
328 System.arraycopy(pathEls, 2, newArray, 1, len - 1);
335 public static class AuthRefInfo extends AuthRefConfigInfo {
337 public Property getProperty() {
341 public void setProperty(Property property) {
342 this.property = property;
346 public AuthRefInfo(String displayName, String schema, String fullPath, String[] pathEls, Property prop) {
347 super(displayName, schema, fullPath, pathEls);
348 this.property = prop;
351 public AuthRefInfo(AuthRefConfigInfo arci, Property prop) {
353 this.property = prop;
357 private static final Logger logger = LoggerFactory.getLogger(RefNameServiceUtils.class);
358 private static ArrayList<String> refNameServiceTypes = null;
360 public static void updateRefNamesInRelations(
361 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
362 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
363 CoreSessionInterface repoSession,
365 String newRefName) throws Exception {
367 // First, look for and update all the places where the refName is the "subject" of the relationship
369 RelationUtils.updateRefNamesInRelations(ctx, repoClient, repoSession, IRelationsManager.SUBJECT_REFNAME, oldRefName, newRefName);
372 // Next, look for and update all the places where the refName is the "object" of the relationship
374 RelationUtils.updateRefNamesInRelations(ctx, repoClient, repoSession, IRelationsManager.OBJECT_REFNAME, oldRefName, newRefName);
377 public static List<AuthRefConfigInfo> getConfiguredAuthorityRefs(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
378 List<String> authRefFields = ((AbstractServiceContextImpl) ctx).getAllPartsPropertyValues(
379 ServiceBindingUtils.AUTH_REF_PROP, ServiceBindingUtils.QUALIFIED_PROP_NAMES);
380 ArrayList<AuthRefConfigInfo> authRefsInfo = new ArrayList<AuthRefConfigInfo>(authRefFields.size());
381 for (String spec : authRefFields) {
382 AuthRefConfigInfo arci = new AuthRefConfigInfo(spec);
383 authRefsInfo.add(arci);
388 public static AuthorityRefDocList getAuthorityRefDocs(
389 CoreSessionInterface repoSession,
390 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
391 UriTemplateRegistry uriTemplateRegistry,
392 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
393 List<String> serviceTypes,
395 String refPropName, // authRef or termRef, authorities or vocab terms.
396 DocumentFilter filter, boolean computeTotal)
397 throws DocumentException, DocumentNotFoundException {
398 AuthorityRefDocList wrapperList = new AuthorityRefDocList();
399 AbstractCommonList commonList = (AbstractCommonList) wrapperList;
400 int pageNum = filter.getStartPage();
401 int pageSize = filter.getPageSize();
403 List<AuthorityRefDocList.AuthorityRefDocItem> list =
404 wrapperList.getAuthorityRefDocItem();
406 Map<String, ServiceBindingType> queriedServiceBindings = new HashMap<String, ServiceBindingType>();
407 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService = new HashMap<String, List<AuthRefConfigInfo>>();
409 NuxeoRepositoryClientImpl nuxeoRepoClient = (NuxeoRepositoryClientImpl) repoClient;
411 // Ignore any provided page size and number query parameters in
412 // the following call, as they pertain to the list of authority
413 // references to be returned, not to the list of documents to be
414 // scanned for those references.
416 // Get a list of possibly referencing documents. This list is
417 // lazily loaded, page by page. Ideally, only one page will
418 // need to be loaded to fill one page of results. Some number
419 // of possibly referencing documents will be false positives,
420 // so use a page size of double the requested page size to
421 // account for those.
422 DocumentModelList docList = findAllAuthorityRefDocs(ctx, repoClient, repoSession,
423 serviceTypes, refName, refPropName, queriedServiceBindings, authRefFieldsByService,
424 filter.getWhereClause(), null, 2*pageSize, computeTotal);
426 if (docList == null) { // found no authRef fields - nothing to process
430 // set the fieldsReturned list. Even though this is a fixed schema, app layer treats
431 // this like other abstract common lists
433 * <xs:element name="docType" type="xs:string" minOccurs="1" />
434 * <xs:element name="docId" type="xs:string" minOccurs="1" />
435 * <xs:element name="docNumber" type="xs:string" minOccurs="0" />
436 * <xs:element name="docName" type="xs:string" minOccurs="0" />
437 * <xs:element name="sourceField" type="xs:string" minOccurs="1" />
438 * <xs:element name="uri" type="xs:anyURI" minOccurs="1" />
439 * <xs:element name="refName" type="xs:String" minOccurs="1" />
440 * <xs:element name="updatedAt" type="xs:string" minOccurs="1" />
441 * <xs:element name="workflowState" type="xs:string" minOccurs="1"
444 String fieldList = "docType|docId|docNumber|docName|sourceField|uri|refName|updatedAt|workflowState"; // FIXME: Should not be hard-coded string
445 commonList.setFieldsReturned(fieldList);
447 // As a side-effect, the method called below modifies the value of
448 // the 'list' variable, which holds the list of references to
449 // an authority item.
451 // There can be more than one reference to a particular authority
452 // item within any individual document scanned, so the number of
453 // authority references may potentially exceed the total number
454 // of documents scanned.
456 // Strip off displayName and only match the base, so we get references to all
457 // the NPTs as well as the PT.
458 String strippedRefName = RefNameUtils.stripAuthorityTermDisplayName(refName);
460 // *** Need to pass in pagination info here.
461 int nRefsFound = processRefObjsDocListForList(docList, ctx.getTenantId(), strippedRefName,
462 queriedServiceBindings, authRefFieldsByService, // the actual list size needs to be updated to the size of "list"
463 list, pageSize, pageNum);
465 commonList.setPageSize(pageSize);
467 // Values returned in the pagination block above the list items
468 // need to reflect the number of references to authority items
469 // returned, rather than the number of documents originally scanned
470 // to find such references.
471 // This will be an estimate only...
472 commonList.setPageNum(pageNum);
473 commonList.setTotalItems(nRefsFound); // Accurate if total was scanned, otherwise, just an estimate
474 commonList.setItemsInPage(list.size());
476 /* Pagination is now handled in the processing step
477 // Slice the list to return only the specified page of items
478 // in the list results.
480 // FIXME: There may well be a pattern-based way to do this
481 // in our framework, and if we can eliminate much of the
482 // non-DRY code below, that would be desirable.
487 // Return all results if pageSize is 0.
490 endIndex = list.size();
492 startIndex = pageNum * pageSize;
495 // Return an empty list when the start of the requested page is
496 // beyond the last item in the list.
497 if (startIndex > list.size()) {
498 wrapperList.getAuthorityRefDocItem().clear();
499 commonList.setItemsInPage(wrapperList.getAuthorityRefDocItem().size());
503 // Otherwise, return a list of items from the start of the specified
504 // page through the last item on that page, or otherwise through the
505 // last item in the entire list, if that occurs earlier than the end
506 // of the specified page.
508 int pageEndIndex = ((startIndex + pageSize));
509 endIndex = (pageEndIndex > list.size()) ? list.size() : pageEndIndex;
512 // Slice the list to return only the specified page of results.
513 // Note: the second argument to List.subList(), endIndex, is
514 // exclusive of the item at its index position, reflecting the
515 // zero-index nature of the list.
516 List<AuthorityRefDocList.AuthorityRefDocItem> currentPageList =
517 new ArrayList<AuthorityRefDocList.AuthorityRefDocItem>(list.subList(startIndex, endIndex));
518 wrapperList.getAuthorityRefDocItem().clear();
519 wrapperList.getAuthorityRefDocItem().addAll(currentPageList);
520 commonList.setItemsInPage(currentPageList.size());
523 if (logger.isDebugEnabled() && (nRefsFound < docList.size())) {
524 logger.debug("Internal curiosity: got fewer matches of refs than # docs matched..."); // We found a ref to ourself and have excluded it.
526 } catch (Exception e) {
527 logger.error("Could not retrieve a list of documents referring to the specified authority item", e);
534 private static ArrayList<String> getRefNameServiceTypes() {
535 if (refNameServiceTypes == null) {
536 refNameServiceTypes = new ArrayList<String>();
537 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_AUTHORITY);
538 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_OBJECT);
539 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_PROCEDURE);
541 return refNameServiceTypes;
544 // Seems like a good value - no real data to set this well.
545 // Note: can set this value lower during debugging; e.g. to 3 - ADR 2012-07-10
546 private static final int N_OBJS_TO_UPDATE_PER_LOOP = 100;
548 public static int updateAuthorityRefDocs(
549 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
550 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
551 CoreSessionInterface repoSession,
554 String refPropName) throws Exception {
555 Map<String, ServiceBindingType> queriedServiceBindings = new HashMap<String, ServiceBindingType>();
556 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService = new HashMap<String, List<AuthRefConfigInfo>>();
561 int docsInCurrentPage = 0;
562 final String WHERE_CLAUSE_ADDITIONS_VALUE = null;
563 final String ORDER_BY_VALUE = CollectionSpaceClient.CORE_CREATED_AT // "collectionspace_core:createdAt";
564 + ", " + IQueryManager.NUXEO_UUID; // CSPACE-6333: Add secondary sort on uuid, in case records have the same createdAt timestamp.
566 if (repoClient instanceof NuxeoRepositoryClientImpl == false) {
567 throw new InternalError("updateAuthorityRefDocs() called with unknown repoClient type!");
570 try { // REM - How can we deal with transaction and timeout issues here?
571 final int pageSize = N_OBJS_TO_UPDATE_PER_LOOP;
572 DocumentModelList docList;
573 boolean morePages = true;
576 docList = findAuthorityRefDocs(ctx, repoClient, repoSession,
577 getRefNameServiceTypes(), oldRefName, refPropName,
578 queriedServiceBindings, authRefFieldsByService, WHERE_CLAUSE_ADDITIONS_VALUE, ORDER_BY_VALUE, pageSize, currentPage, false);
580 if (docList == null) {
581 logger.debug("updateAuthorityRefDocs: no documents could be found that referenced the old refName");
584 docsInCurrentPage = docList.size();
585 logger.debug("updateAuthorityRefDocs: current page=" + currentPage + " documents included in page=" + docsInCurrentPage);
586 if (docsInCurrentPage == 0) {
587 logger.debug("updateAuthorityRefDocs: no more documents requiring refName updates could be found");
590 if (docsInCurrentPage < pageSize) {
591 logger.debug("updateAuthorityRefDocs: assuming no more documents requiring refName updates will be found, as docsInCurrentPage < pageSize");
595 // Only match complete refNames - unless and until we decide how to resolve changes
596 // to NPTs we will defer that and only change PTs or refNames as passed in.
597 int nRefsFoundThisPage = processRefObjsDocListForUpdate(ctx, docList, ctx.getTenantId(), oldRefName,
598 queriedServiceBindings, authRefFieldsByService, // Perform the refName updates on the list of document models
600 if (nRefsFoundThisPage > 0) {
601 ((NuxeoRepositoryClientImpl) repoClient).saveDocListWithoutHandlerProcessing(ctx, repoSession, docList, true); // Flush the document model list out to Nuxeo storage
602 nRefsFound += nRefsFoundThisPage;
605 // FIXME: Per REM, set a limit of num objects - something like
606 // 1000K objects - and also add a log Warning after some threshold
607 docsScanned += docsInCurrentPage;
613 } catch (Exception e) {
614 logger.error("Internal error updating the AuthorityRefDocs: " + e.getLocalizedMessage());
615 logger.debug(Tools.errorToString(e, true));
618 logger.debug("updateAuthorityRefDocs replaced a total of " + nRefsFound + " authority references, within as many as " + docsScanned + " scanned document(s)");
622 private static DocumentModelList findAllAuthorityRefDocs(
623 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
624 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
625 CoreSessionInterface repoSession, List<String> serviceTypes,
628 Map<String, ServiceBindingType> queriedServiceBindings,
629 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
630 String whereClauseAdditions,
631 String orderByClause,
633 boolean computeTotal) throws DocumentException, DocumentNotFoundException {
635 return new LazyAuthorityRefDocList(ctx, repoClient, repoSession,
636 serviceTypes, refName, refPropName, queriedServiceBindings, authRefFieldsByService,
637 whereClauseAdditions, orderByClause, pageSize, computeTotal);
640 protected static DocumentModelList findAuthorityRefDocs(
641 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
642 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
643 CoreSessionInterface repoSession, List<String> serviceTypes,
646 Map<String, ServiceBindingType> queriedServiceBindings,
647 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
648 String whereClauseAdditions,
649 String orderByClause,
652 boolean computeTotal) throws DocumentException, DocumentNotFoundException {
654 // Get the service bindings for this tenant
655 TenantBindingConfigReaderImpl tReader =
656 ServiceMain.getInstance().getTenantBindingConfigReader();
657 // We need to get all the procedures, authorities, and objects.
658 List<ServiceBindingType> servicebindings = tReader.getServiceBindingsByType(ctx.getTenantId(), serviceTypes);
659 if (servicebindings == null || servicebindings.isEmpty()) {
660 logger.error("RefNameServiceUtils.getAuthorityRefDocs: No services bindings found, cannot proceed!");
663 // Filter the list for current user rights
664 servicebindings = SecurityUtils.getReadableServiceBindingsForCurrentUser(servicebindings);
666 ArrayList<String> docTypes = new ArrayList<String>();
668 String query = computeWhereClauseForAuthorityRefDocs(refName, refPropName, docTypes, servicebindings, // REM - Side effect that docTypes, authRefFieldsByService, and queriedServiceBindings get set/change. Any others?
669 queriedServiceBindings, authRefFieldsByService);
670 if (query == null) { // found no authRef fields - nothing to query
673 // Additional qualifications, like workflow state
674 if (Tools.notBlank(whereClauseAdditions)) {
675 query += " AND " + whereClauseAdditions;
677 // Now we have to issue the search
678 NuxeoRepositoryClientImpl nuxeoRepoClient = (NuxeoRepositoryClientImpl) repoClient;
679 DocumentWrapper<DocumentModelList> docListWrapper = nuxeoRepoClient.findDocs(ctx, repoSession,
680 docTypes, query, orderByClause, pageSize, pageNum, computeTotal);
681 // Now we gather the info for each document into the list and return
682 DocumentModelList docList = docListWrapper.getWrappedObject();
685 private static final boolean READY_FOR_COMPLEX_QUERY = true;
687 private static String computeWhereClauseForAuthorityRefDocs(
690 ArrayList<String> docTypes,
691 List<ServiceBindingType> servicebindings,
692 Map<String, ServiceBindingType> queriedServiceBindings,
693 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService) {
695 boolean fFirst = true;
696 List<String> authRefFieldPaths;
697 for (ServiceBindingType sb : servicebindings) {
698 // Gets the property names for each part, qualified with the part label (which
699 // is also the table name, the way that the repository works).
701 ServiceBindingUtils.getAllPartsPropertyValues(sb,
702 refPropName, ServiceBindingUtils.QUALIFIED_PROP_NAMES);
703 if (authRefFieldPaths.isEmpty()) {
706 ArrayList<AuthRefConfigInfo> authRefsInfo = new ArrayList<AuthRefConfigInfo>();
707 for (String spec : authRefFieldPaths) {
708 AuthRefConfigInfo arci = new AuthRefConfigInfo(spec);
709 authRefsInfo.add(arci);
712 String docType = sb.getObject().getName();
713 queriedServiceBindings.put(docType, sb);
714 authRefFieldsByService.put(docType, authRefsInfo);
715 docTypes.add(docType);
718 if (fFirst) { // found no authRef fields - nothing to query
721 // We used to build a complete matches query, but that was too complex.
722 // Just build a keyword query based upon some key pieces - the urn syntax elements and the shortID
723 // Note that this will also match the Item itself, but that will get filtered out when
724 // we compute actual matches.
725 AuthorityTermInfo authTermInfo = RefNameUtils.parseAuthorityTermInfo(refName);
727 String keywords = RefNameUtils.URN_PREFIX
728 + " AND " + (authTermInfo.inAuthority.name != null
729 ? authTermInfo.inAuthority.name : authTermInfo.inAuthority.csid)
730 + " AND " + (authTermInfo.name != null
731 ? authTermInfo.name : authTermInfo.csid); // REM - This seems likely to cause trouble? We should consider searching for the full refname -excluding the display name suffix?
733 String whereClauseStr = QueryManager.createWhereClauseFromKeywords(keywords);
735 if (logger.isTraceEnabled()) {
736 logger.trace("The 'where' clause to find refObjs is: ", whereClauseStr);
739 return whereClauseStr;
742 // TODO there are multiple copies of this that should be put somewhere common.
743 protected static String getRefname(DocumentModel docModel) throws ClientException {
744 String result = (String)docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
745 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
749 private static int processRefObjsDocListForUpdate(
751 DocumentModelList docList,
754 Map<String, ServiceBindingType> queriedServiceBindings,
755 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
756 String newAuthorityRefName) {
757 boolean matchBaseOnly = false;
759 if (ctx.shouldForceUpdateRefnameReferences() == true) {
760 refName = RefNameUtils.stripAuthorityTermDisplayName(refName);
761 matchBaseOnly = true;
764 return processRefObjsDocList(docList, tenantId, refName, matchBaseOnly, queriedServiceBindings,
765 authRefFieldsByService, null, 0, 0, newAuthorityRefName);
768 private static int processRefObjsDocListForList(
769 DocumentModelList docList,
772 Map<String, ServiceBindingType> queriedServiceBindings,
773 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
774 List<AuthorityRefDocList.AuthorityRefDocItem> list,
775 int pageSize, int pageNum) {
776 return processRefObjsDocList(docList, tenantId, refName, true, queriedServiceBindings,
777 authRefFieldsByService, list, pageSize, pageNum, null);
782 * Runs through the list of found docs, processing them. If list is
783 * non-null, then processing means gather the info for items. If list is
784 * null, and newRefName is non-null, then processing means replacing and
785 * updating. If processing/updating, this must be called in the context of
786 * an open session, and caller must release Session after calling this.
789 private static int processRefObjsDocList(
790 DocumentModelList docList,
793 boolean matchBaseOnly,
794 Map<String, ServiceBindingType> queriedServiceBindings,
795 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
796 List<AuthorityRefDocList.AuthorityRefDocItem> list,
797 int pageSize, int pageNum, // Only used when constructing a list.
798 String newAuthorityRefName) {
799 UriTemplateRegistry registry = ServiceMain.getInstance().getUriTemplateRegistry();
800 Iterator<DocumentModel> iter = docList.iterator();
801 int nRefsFoundTotal = 0;
802 boolean foundSelf = false;
804 // When paginating results, we have to guess at the total. First guess is the number of docs returned
805 // by the query. However, this returns some false positives, so may be high.
806 // In addition, we can match multiple fields per doc, so this may be low. Fun, eh?
807 int nDocsReturnedInQuery = (int)docList.totalSize();
808 int nDocsProcessed = 0;
809 int firstItemInPage = pageNum*pageSize;
810 while (iter.hasNext()) {
811 DocumentModel docModel = iter.next();
812 AuthorityRefDocList.AuthorityRefDocItem ilistItem;
814 String docType = docModel.getDocumentType().getName(); // REM - This will be a tentant qualified document type
815 docType = ServiceBindingUtils.getUnqualifiedTenantDocType(docType);
816 ServiceBindingType sb = queriedServiceBindings.get(docType);
818 throw new RuntimeException(
819 "getAuthorityRefDocs: No Service Binding for docType: " + docType);
822 if (list == null) { // no list - should be update refName case.
823 if (newAuthorityRefName == null) {
824 throw new InternalError("processRefObjsDocList() called with neither an itemList nor a new RefName!");
828 firstItemInPage = 0; // Do not paginate if updating, rather than building list
829 } else { // Have a list - refObjs case
830 if (newAuthorityRefName != null) {
831 throw new InternalError("processRefObjsDocList() called with both an itemList and a new RefName!");
833 if(firstItemInPage > 100) {
834 logger.warn("Processing a large offset (size:{}, num:{}) for refObjs - will be expensive!!!",
837 // Note that we have to go through check all the fields to determine the actual page start
838 ilistItem = new AuthorityRefDocList.AuthorityRefDocItem();
839 String csid = NuxeoUtils.getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
841 String itemRefName = getRefname(docModel);
842 ilistItem.setRefName(itemRefName);
843 } catch (ClientException ce) {
844 throw new RuntimeException(
845 "processRefObjsDocList: Problem fetching refName from item Object: "
846 + ce.getLocalizedMessage());
848 ilistItem.setDocId(csid);
850 UriTemplateRegistryKey key = new UriTemplateRegistryKey(tenantId, docType);
851 StoredValuesUriTemplate template = registry.get(key);
852 if (template != null) {
853 Map<String, String> additionalValues = new HashMap<String, String>();
854 if (template.getUriTemplateType() == UriTemplateFactory.RESOURCE) {
855 additionalValues.put(UriTemplateFactory.IDENTIFIER_VAR, csid);
856 uri = template.buildUri(additionalValues);
857 } else if (template.getUriTemplateType() == UriTemplateFactory.ITEM) {
859 String inAuthorityCsid = (String) NuxeoUtils.getProperyValue(docModel, "inAuthority"); //docModel.getPropertyValue("inAuthority"); // AuthorityItemJAXBSchema.IN_AUTHORITY
860 additionalValues.put(UriTemplateFactory.IDENTIFIER_VAR, inAuthorityCsid);
861 additionalValues.put(UriTemplateFactory.ITEM_IDENTIFIER_VAR, csid);
862 uri = template.buildUri(additionalValues);
863 } catch (Exception e) {
864 logger.warn("Could not extract inAuthority property from authority item record: " + e.getMessage());
866 } else if (template.getUriTemplateType() == UriTemplateFactory.CONTACT) {
867 // FIXME: Generating contact sub-resource URIs requires additional work,
868 // as a follow-on to CSPACE-5271 - ADR 2012-08-16
869 // Sets the default (empty string) value for uri, for now
871 logger.warn("Unrecognized URI template type = " + template.getUriTemplateType());
872 // Sets the default (empty string) value for uri
874 } else { // (if template == null)
875 logger.warn("Could not retrieve URI template from registry via tenant ID "
876 + tenantId + " and docType " + docType);
877 // Sets the default (empty string) value for uri
879 ilistItem.setUri(uri);
881 ilistItem.setWorkflowState(docModel.getCurrentLifeCycleState());
882 ilistItem.setUpdatedAt(NuxeoDocumentModelHandler.getUpdatedAtAsString(docModel));
883 } catch (Exception e) {
884 logger.error("Error getting core values for doc [" + csid + "]: " + e.getLocalizedMessage());
886 ilistItem.setDocType(docType);
887 ilistItem.setDocNumber(
888 ServiceBindingUtils.getMappedFieldInDoc(sb, ServiceBindingUtils.OBJ_NUMBER_PROP, docModel));
889 ilistItem.setDocName(
890 ServiceBindingUtils.getMappedFieldInDoc(sb, ServiceBindingUtils.OBJ_NAME_PROP, docModel));
892 // Now, we have to loop over the authRefFieldsByService to figure out
893 // out which field(s) matched this.
894 List<AuthRefConfigInfo> matchingAuthRefFields = authRefFieldsByService.get(docType);
895 if (matchingAuthRefFields == null || matchingAuthRefFields.isEmpty()) {
896 throw new RuntimeException(
897 "getAuthorityRefDocs: internal logic error: can't fetch authRefFields for DocType.");
899 //String authRefAncestorField = "";
900 //String authRefDescendantField = "";
901 //String sourceField = "";
903 ArrayList<RefNameServiceUtils.AuthRefInfo> foundProps = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
905 findAuthRefPropertiesInDoc(docModel, matchingAuthRefFields, refName, matchBaseOnly, foundProps); // REM - side effect that foundProps is set
906 if(!foundProps.isEmpty()) {
907 int nRefsFoundInDoc = 0;
908 for (RefNameServiceUtils.AuthRefInfo ari : foundProps) {
909 if (ilistItem != null) {
910 // So this is a true positive, and not a false one. We have to consider pagination now.
911 if(nRefsFoundTotal >= firstItemInPage) { // skipped enough already
912 if (nRefsFoundInDoc == 0) { // First one?
913 ilistItem.setSourceField(ari.getQualifiedDisplayName());
914 } else { // duplicates from one object
915 ilistItem = cloneAuthRefDocItem(ilistItem, ari.getQualifiedDisplayName());
918 nRefsFoundInDoc++; // Only increment if processed, or clone logic above will fail
920 } else { // update refName case
921 Property propToUpdate = ari.getProperty();
922 propToUpdate.setValue(newAuthorityRefName);
924 nRefsFoundTotal++; // Whether we processed or not, we found - essential to pagination logic
926 } else if(ilistItem != null) {
927 String docRefName = ilistItem.getRefName();
929 (docRefName!=null && docRefName.startsWith(refName))
930 :refName.equals(docRefName)) {
931 // We found the self for an item
933 logger.debug("getAuthorityRefDocs: Result: "
934 + docType + " [" + NuxeoUtils.getCsid(docModel)
935 + "] appears to be self for: ["
938 logger.debug("getAuthorityRefDocs: Result: "
939 + docType + " [" + NuxeoUtils.getCsid(docModel)
940 + "] does not reference ["
944 } catch (ClientException ce) {
945 throw new RuntimeException(
946 "getAuthorityRefDocs: Problem fetching values from repo: " + ce.getLocalizedMessage());
949 // Done processing that doc. Are we done with the whole page?
950 // Note pageSize <=0 means do them all
951 if((pageSize > 0) && ((nRefsFoundTotal-firstItemInPage)>=pageSize)) {
952 // Quitting early, so we need to estimate the total. Assume one per doc
953 // for the rest of the docs we matched in the query
954 int unprocessedDocs = nDocsReturnedInQuery - nDocsProcessed;
955 if(unprocessedDocs>0) {
956 // We generally match ourselves in the keyword search. If we already saw ourselves
957 // then do not try to correct for this. Otherwise, decrement the total.
958 // Yes, this is fairly goofy, but the whole estimation mechanism is goofy.
961 nRefsFoundTotal += unprocessedDocs;
965 } // close while(iterator)
966 return nRefsFoundTotal;
970 * Clone an AuthorityRefDocItem which is a JAX-B generated class. Be sure we're copying every field defined in the XSD (XML Schema) that is
971 * found here services\jaxb\src\main\resources\authorityrefdocs.xsd
976 private static AuthorityRefDocList.AuthorityRefDocItem cloneAuthRefDocItem(
977 AuthorityRefDocList.AuthorityRefDocItem ilistItem, String sourceField) {
978 AuthorityRefDocList.AuthorityRefDocItem newlistItem = new AuthorityRefDocList.AuthorityRefDocItem();
979 newlistItem.setDocId(ilistItem.getDocId());
980 newlistItem.setDocName(ilistItem.getDocName());
981 newlistItem.setDocNumber(ilistItem.getDocNumber());
982 newlistItem.setDocType(ilistItem.getDocType());
983 newlistItem.setUri(ilistItem.getUri());
984 newlistItem.setSourceField(sourceField);
985 newlistItem.setRefName(ilistItem.getRefName());
986 newlistItem.setUpdatedAt(ilistItem.getUpdatedAt());
987 newlistItem.setWorkflowState(ilistItem.getWorkflowState());
991 public static List<AuthRefInfo> findAuthRefPropertiesInDoc(
992 DocumentModel docModel,
993 List<AuthRefConfigInfo> authRefFieldInfoList,
994 String refNameToMatch,
995 List<AuthRefInfo> foundProps) {
996 return findAuthRefPropertiesInDoc(docModel, authRefFieldInfoList,
997 refNameToMatch, false, foundProps);
1000 public static List<AuthRefInfo> findAuthRefPropertiesInDoc(
1001 DocumentModel docModel,
1002 List<AuthRefConfigInfo> authRefFieldInfoList,
1003 String refNameToMatch,
1004 boolean matchBaseOnly,
1005 List<AuthRefInfo> authRefInfoList) {
1006 // Assume that authRefFieldInfo is keyed by the field name (possibly mapped for UI)
1007 // and the values are elPaths to the field, where intervening group structures in
1008 // lists of complex structures are replaced with "*". Thus, valid paths include
1009 // the following (note that the ServiceBindingUtils prepend schema names to configured values):
1010 // "schemaname:fieldname"
1011 // "schemaname:scalarlistname"
1012 // "schemaname:complexfieldname/fieldname"
1013 // "schemaname:complexlistname/*/fieldname"
1014 // "schemaname:complexlistname/*/scalarlistname"
1015 // "schemaname:complexlistname/*/complexfieldname/fieldname"
1016 // "schemaname:complexlistname/*/complexlistname/*/fieldname"
1018 for (AuthRefConfigInfo arci : authRefFieldInfoList) {
1020 // Get first property and work down as needed.
1021 Property prop = docModel.getProperty(arci.pathEls[0]);
1022 findAuthRefPropertiesInProperty(authRefInfoList, prop, arci, 0, refNameToMatch, matchBaseOnly);
1023 } catch (Exception e) {
1024 logger.error("Problem fetching property: " + arci.pathEls[0]);
1027 return authRefInfoList;
1030 private static List<AuthRefInfo> findAuthRefPropertiesInProperty(
1031 List<AuthRefInfo> authRefInfoList,
1033 AuthRefConfigInfo arci,
1034 int pathStartIndex, // Supports recursion and we work down the path
1035 String refNameToMatch,
1036 boolean matchBaseOnly ) {
1037 if (pathStartIndex >= arci.pathEls.length) {
1038 throw new ArrayIndexOutOfBoundsException("Index = " + pathStartIndex + " for path: "
1039 + arci.pathEls.toString());
1041 AuthRefInfo ari = null;
1043 return authRefInfoList;
1046 if (prop instanceof StringProperty) { // scalar string
1047 addARIifMatches(refNameToMatch, matchBaseOnly, arci, prop, authRefInfoList); // REM - Side effect that foundProps gets changed/updated
1048 } else if (prop instanceof List) {
1049 List<Property> propList = (List<Property>) prop;
1050 // run through list. Must either be list of Strings, or Complex
1051 for (Property listItemProp : propList) {
1052 if (listItemProp instanceof StringProperty) {
1053 if (arci.pathEls.length - pathStartIndex != 1) {
1054 logger.error("Configuration for authRefs does not match schema structure: "
1055 + arci.pathEls.toString());
1058 addARIifMatches(refNameToMatch, matchBaseOnly, arci, listItemProp, authRefInfoList);
1060 } else if (listItemProp.isComplex()) {
1061 // Just recurse to handle this. Note that since this is a list of complex,
1062 // which should look like listName/*/... we add 2 to the path start index
1063 findAuthRefPropertiesInProperty(authRefInfoList, listItemProp, arci,
1064 pathStartIndex + 2, refNameToMatch, matchBaseOnly);
1066 logger.error("Configuration for authRefs does not match schema structure: "
1067 + arci.pathEls.toString());
1071 } else if (prop.isComplex()) {
1072 String localPropName = arci.pathEls[pathStartIndex];
1074 Property localProp = prop.get(localPropName);
1075 // Now just recurse, pushing down the path 1 step
1076 findAuthRefPropertiesInProperty(authRefInfoList, localProp, arci,
1077 pathStartIndex, refNameToMatch, matchBaseOnly);
1078 } catch (PropertyNotFoundException pnfe) {
1079 logger.error("Could not find property: [" + localPropName + "] in path: "
1080 + arci.getFullPath());
1081 // Fall through - ari will be null and we will continue...
1084 logger.error("Configuration for authRefs does not match schema structure: "
1085 + arci.pathEls.toString());
1089 authRefInfoList.add(ari); //FIXME: REM - This is dead code. 'ari' is never touched after being initalized to null. Why?
1092 return authRefInfoList;
1095 private static void addARIifMatches(
1096 String refNameToMatch,
1097 boolean matchBaseOnly,
1098 AuthRefConfigInfo arci,
1100 List<AuthRefInfo> authRefInfoList) {
1101 // Need to either match a passed refName
1102 // OR have no refName to match but be non-empty
1104 String value = (String) prop.getValue();
1105 if (((refNameToMatch != null) &&
1107 (value!=null && value.startsWith(refNameToMatch))
1108 :refNameToMatch.equals(value)))
1109 || ((refNameToMatch == null) && Tools.notBlank(value))) {
1111 logger.debug("Found a match on property: " + prop.getPath() + " with value: [" + value + "]");
1112 AuthRefInfo ari = new AuthRefInfo(arci, prop);
1113 authRefInfoList.add(ari);
1115 } catch (PropertyException pe) {
1116 logger.debug("PropertyException on: " + prop.getPath() + pe.getLocalizedMessage());
1120 public static String buildWhereForAuthByName(String authorityCommonSchemaName, String name) {
1121 return authorityCommonSchemaName
1122 + ":" + AuthorityJAXBSchema.SHORT_IDENTIFIER
1123 + "='" + name + "'";
1127 * Build an NXQL query for finding an item by its short ID
1129 * @param authorityItemCommonSchemaName
1134 public static String buildWhereForAuthItemByName(String authorityItemCommonSchemaName, String shortId, String parentcsid) {
1135 String result = null;
1137 result = String.format("%s:%s='%s'", authorityItemCommonSchemaName, AuthorityItemJAXBSchema.SHORT_IDENTIFIER, shortId);
1139 // Technically, we don't need the parent CSID since the short ID is unique so it can be null
1141 if (parentcsid != null) {
1142 result = String.format("%s AND %s:%s='%s'",
1143 result, authorityItemCommonSchemaName, AuthorityItemJAXBSchema.IN_AUTHORITY, parentcsid);
1150 * Identifies whether the refName was found in the supplied field. If passed
1151 * a new RefName, will set that into fields in which the old one was found.
1153 * Only works for: * Scalar fields * Repeatable scalar fields (aka
1154 * multi-valued fields)
1156 * Does not work for: * Structured fields (complexTypes) * Repeatable
1157 * structured fields (repeatable complexTypes) private static int
1158 * refNameFoundInField(String oldRefName, Property fieldValue, String
1159 * newRefName) { int nFound = 0; if (fieldValue instanceof List) {
1160 * List<Property> fieldValueList = (List) fieldValue; for (Property
1161 * listItemValue : fieldValueList) { try { if ((listItemValue instanceof
1162 * StringProperty) &&
1163 * oldRefName.equalsIgnoreCase((String)listItemValue.getValue())) {
1164 * nFound++; if(newRefName!=null) { fieldValue.setValue(newRefName); } else
1165 * { // We cannot quit after the first, if we are replacing values. // If we
1166 * are just looking (not replacing), finding one is enough. break; } } }
1167 * catch( PropertyException pe ) {} } } else { try { if ((fieldValue
1168 * instanceof StringProperty) &&
1169 * oldRefName.equalsIgnoreCase((String)fieldValue.getValue())) { nFound++;
1170 * if(newRefName!=null) { fieldValue.setValue(newRefName); } } } catch(
1171 * PropertyException pe ) {} } return nFound; }