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 org.nuxeo.ecm.core.api.ClientException;
32 import org.nuxeo.ecm.core.api.DocumentModel;
33 import org.nuxeo.ecm.core.api.DocumentModelList;
34 import org.nuxeo.ecm.core.api.model.Property;
35 import org.nuxeo.ecm.core.api.model.PropertyException;
36 import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
37 import org.nuxeo.ecm.core.api.model.impl.primitives.StringProperty;
38 import org.nuxeo.ecm.core.api.CoreSession;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41 import org.collectionspace.services.client.CollectionSpaceClient;
42 import org.collectionspace.services.client.IQueryManager;
43 import org.collectionspace.services.client.IRelationsManager;
44 import org.collectionspace.services.client.PoxPayloadIn;
45 import org.collectionspace.services.client.PoxPayloadOut;
46 import org.collectionspace.services.common.ServiceMain;
47 import org.collectionspace.services.common.StoredValuesUriTemplate;
48 import org.collectionspace.services.common.UriTemplateFactory;
49 import org.collectionspace.services.common.UriTemplateRegistry;
50 import org.collectionspace.services.common.UriTemplateRegistryKey;
51 import org.collectionspace.services.common.context.ServiceContext;
52 import org.collectionspace.services.common.context.AbstractServiceContextImpl;
53 import org.collectionspace.services.common.api.RefNameUtils;
54 import org.collectionspace.services.common.api.Tools;
55 import org.collectionspace.services.common.api.RefNameUtils.AuthorityTermInfo;
56 import org.collectionspace.services.common.authorityref.AuthorityRefDocList;
57 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
58 import org.collectionspace.services.common.context.ServiceBindingUtils;
59 import org.collectionspace.services.common.document.DocumentException;
60 import org.collectionspace.services.common.document.DocumentFilter;
61 import org.collectionspace.services.common.document.DocumentNotFoundException;
62 import org.collectionspace.services.common.document.DocumentUtils;
63 import org.collectionspace.services.common.document.DocumentWrapper;
64 import org.collectionspace.services.common.query.QueryManager;
65 import org.collectionspace.services.common.relation.RelationUtils;
66 import org.collectionspace.services.common.repository.RepositoryClient;
67 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
68 import org.collectionspace.services.nuxeo.client.java.NuxeoDocumentModelHandler;
69 import org.collectionspace.services.nuxeo.client.java.RepositoryClientImpl;
70 import org.collectionspace.services.common.security.SecurityUtils;
71 import org.collectionspace.services.config.service.ServiceBindingType;
72 import org.collectionspace.services.jaxb.AbstractCommonList;
73 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
76 * RefNameServiceUtils is a collection of services utilities related to refName
79 * $LastChangedRevision: $ $LastChangedDate: $
81 public class RefNameServiceUtils {
83 public static class AuthRefConfigInfo {
85 public String getQualifiedDisplayName() {
86 return (Tools.isBlank(schema))
87 ? displayName : DocumentUtils.appendSchemaName(schema, displayName);
90 public String getDisplayName() {
94 public void setDisplayName(String displayName) {
95 this.displayName = displayName;
100 public String getSchema() {
104 public void setSchema(String schema) {
105 this.schema = schema;
108 public String getFullPath() {
112 public void setFullPath(String fullPath) {
113 this.fullPath = fullPath;
116 protected String[] pathEls;
118 public AuthRefConfigInfo(AuthRefConfigInfo arci) {
119 this.displayName = arci.displayName;
120 this.schema = arci.schema;
121 this.fullPath = arci.fullPath;
122 this.pathEls = arci.pathEls;
123 // Skip the pathElse check, since we are creatign from another (presumably valid) arci.
126 public AuthRefConfigInfo(String displayName, String schema, String fullPath, String[] pathEls) {
127 this.displayName = displayName;
128 this.schema = schema;
129 this.fullPath = fullPath;
130 this.pathEls = pathEls;
134 // Split a config value string like "intakes_common:collector", or
135 // "collectionobjects_common:contentPeoples|contentPeople"
136 // "collectionobjects_common:assocEventGroupList/*/assocEventPlace"
137 // If has a pipe ('|') second part is a displayLabel, and first is path
138 // Otherwise, entry is a path, and can use the last pathElement as displayName
139 // Should be schema qualified.
140 public AuthRefConfigInfo(String configString) {
141 String[] pair = configString.split("\\|", 2);
143 String displayName, fullPath;
144 if (pair.length == 1) {
145 // no label specifier, so we'll defer getting label
147 pathEls = pair[0].split("/");
148 displayName = pathEls[pathEls.length - 1];
151 pathEls = pair[0].split("/");
152 displayName = pair[1];
154 String[] schemaSplit = pathEls[0].split(":", 2);
156 if (schemaSplit.length == 1) { // schema not specified
159 schema = schemaSplit[0];
160 if (pair.length == 1 && pathEls.length == 1) { // simplest case of field in top level schema, no labelll
161 displayName = schemaSplit[1]; // Have to fix up displayName to have no schema
164 this.displayName = displayName;
165 this.schema = schema;
166 this.fullPath = fullPath;
167 this.pathEls = pathEls;
171 protected void checkPathEls() {
172 int len = pathEls.length;
174 throw new InternalError("Bad values in authRef info - caller screwed up:" + fullPath);
176 // Handle case of them putting a leading slash on the path
177 if (len > 1 && pathEls[0].endsWith(":")) {
179 String[] newArray = new String[len];
180 newArray[0] = pathEls[0] + pathEls[1];
182 System.arraycopy(pathEls, 2, newArray, 1, len - 1);
189 public static class AuthRefInfo extends AuthRefConfigInfo {
191 public Property getProperty() {
195 public void setProperty(Property property) {
196 this.property = property;
200 public AuthRefInfo(String displayName, String schema, String fullPath, String[] pathEls, Property prop) {
201 super(displayName, schema, fullPath, pathEls);
202 this.property = prop;
205 public AuthRefInfo(AuthRefConfigInfo arci, Property prop) {
207 this.property = prop;
211 private static final Logger logger = LoggerFactory.getLogger(RefNameServiceUtils.class);
212 private static ArrayList<String> refNameServiceTypes = null;
214 public static void updateRefNamesInRelations(
215 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
216 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
217 CoreSessionInterface repoSession,
219 String newRefName) throws Exception {
221 // First, look for and update all the places where the refName is the "subject" of the relationship
223 RelationUtils.updateRefNamesInRelations(ctx, repoClient, repoSession, IRelationsManager.SUBJECT_REFNAME, oldRefName, newRefName);
226 // Next, look for and update all the places where the refName is the "object" of the relationship
228 RelationUtils.updateRefNamesInRelations(ctx, repoClient, repoSession, IRelationsManager.OBJECT_REFNAME, oldRefName, newRefName);
231 public static List<AuthRefConfigInfo> getConfiguredAuthorityRefs(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
232 List<String> authRefFields = ((AbstractServiceContextImpl) ctx).getAllPartsPropertyValues(
233 ServiceBindingUtils.AUTH_REF_PROP, ServiceBindingUtils.QUALIFIED_PROP_NAMES);
234 ArrayList<AuthRefConfigInfo> authRefsInfo = new ArrayList<AuthRefConfigInfo>(authRefFields.size());
235 for (String spec : authRefFields) {
236 AuthRefConfigInfo arci = new AuthRefConfigInfo(spec);
237 authRefsInfo.add(arci);
242 public static AuthorityRefDocList getAuthorityRefDocs(
243 CoreSessionInterface repoSession,
244 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
245 UriTemplateRegistry uriTemplateRegistry,
246 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
247 List<String> serviceTypes,
249 String refPropName, // authRef or termRef, authorities or vocab terms.
250 DocumentFilter filter, boolean computeTotal)
251 throws DocumentException, DocumentNotFoundException {
252 AuthorityRefDocList wrapperList = new AuthorityRefDocList();
253 AbstractCommonList commonList = (AbstractCommonList) wrapperList;
254 int pageNum = filter.getStartPage();
255 int pageSize = filter.getPageSize();
257 List<AuthorityRefDocList.AuthorityRefDocItem> list =
258 wrapperList.getAuthorityRefDocItem();
260 Map<String, ServiceBindingType> queriedServiceBindings = new HashMap<String, ServiceBindingType>();
261 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService = new HashMap<String, List<AuthRefConfigInfo>>();
263 RepositoryClientImpl nuxeoRepoClient = (RepositoryClientImpl) repoClient;
265 // Ignore any provided page size and number query parameters in
266 // the following call, as they pertain to the list of authority
267 // references to be returned, not to the list of documents to be
268 // scanned for those references.
270 // Get a list of possibly referencing documents. This list is
271 // lazily loaded, page by page. Ideally, only one page will
272 // need to be loaded to fill one page of results. Some number
273 // of possibly referencing documents will be false positives,
274 // so use a page size of double the requested page size to
275 // account for those.
276 DocumentModelList docList = findAllAuthorityRefDocs(ctx, repoClient, repoSession,
277 serviceTypes, refName, refPropName, queriedServiceBindings, authRefFieldsByService,
278 filter.getWhereClause(), null, 2*pageSize, computeTotal);
280 if (docList == null) { // found no authRef fields - nothing to process
284 // set the fieldsReturned list. Even though this is a fixed schema, app layer treats
285 // this like other abstract common lists
287 * <xs:element name="docType" type="xs:string" minOccurs="1" />
288 * <xs:element name="docId" type="xs:string" minOccurs="1" />
289 * <xs:element name="docNumber" type="xs:string" minOccurs="0" />
290 * <xs:element name="docName" type="xs:string" minOccurs="0" />
291 * <xs:element name="sourceField" type="xs:string" minOccurs="1" />
292 * <xs:element name="uri" type="xs:anyURI" minOccurs="1" />
293 * <xs:element name="refName" type="xs:String" minOccurs="1" />
294 * <xs:element name="updatedAt" type="xs:string" minOccurs="1" />
295 * <xs:element name="workflowState" type="xs:string" minOccurs="1"
298 String fieldList = "docType|docId|docNumber|docName|sourceField|uri|refName|updatedAt|workflowState";
299 commonList.setFieldsReturned(fieldList);
301 // As a side-effect, the method called below modifies the value of
302 // the 'list' variable, which holds the list of references to
303 // an authority item.
305 // There can be more than one reference to a particular authority
306 // item within any individual document scanned, so the number of
307 // authority references may potentially exceed the total number
308 // of documents scanned.
310 // Strip off displayName and only match the base, so we get references to all
311 // the NPTs as well as the PT.
312 String strippedRefName = RefNameUtils.stripAuthorityTermDisplayName(refName);
314 // *** Need to pass in pagination info here.
315 int nRefsFound = processRefObjsDocListForList(docList, ctx.getTenantId(), strippedRefName,
316 queriedServiceBindings, authRefFieldsByService, // the actual list size needs to be updated to the size of "list"
317 list, pageSize, pageNum);
319 commonList.setPageSize(pageSize);
321 // Values returned in the pagination block above the list items
322 // need to reflect the number of references to authority items
323 // returned, rather than the number of documents originally scanned
324 // to find such references.
325 // This will be an estimate only...
326 commonList.setPageNum(pageNum);
327 commonList.setTotalItems(nRefsFound); // Accurate if total was scanned, otherwise, just an estimate
328 commonList.setItemsInPage(list.size());
330 /* Pagination is now handled in the processing step
331 // Slice the list to return only the specified page of items
332 // in the list results.
334 // FIXME: There may well be a pattern-based way to do this
335 // in our framework, and if we can eliminate much of the
336 // non-DRY code below, that would be desirable.
341 // Return all results if pageSize is 0.
344 endIndex = list.size();
346 startIndex = pageNum * pageSize;
349 // Return an empty list when the start of the requested page is
350 // beyond the last item in the list.
351 if (startIndex > list.size()) {
352 wrapperList.getAuthorityRefDocItem().clear();
353 commonList.setItemsInPage(wrapperList.getAuthorityRefDocItem().size());
357 // Otherwise, return a list of items from the start of the specified
358 // page through the last item on that page, or otherwise through the
359 // last item in the entire list, if that occurs earlier than the end
360 // of the specified page.
362 int pageEndIndex = ((startIndex + pageSize));
363 endIndex = (pageEndIndex > list.size()) ? list.size() : pageEndIndex;
366 // Slice the list to return only the specified page of results.
367 // Note: the second argument to List.subList(), endIndex, is
368 // exclusive of the item at its index position, reflecting the
369 // zero-index nature of the list.
370 List<AuthorityRefDocList.AuthorityRefDocItem> currentPageList =
371 new ArrayList<AuthorityRefDocList.AuthorityRefDocItem>(list.subList(startIndex, endIndex));
372 wrapperList.getAuthorityRefDocItem().clear();
373 wrapperList.getAuthorityRefDocItem().addAll(currentPageList);
374 commonList.setItemsInPage(currentPageList.size());
377 if (logger.isDebugEnabled() && (nRefsFound < docList.size())) {
378 logger.debug("Internal curiosity: got fewer matches of refs than # docs matched..."); // We found a ref to ourself and have excluded it.
380 } catch (Exception e) {
381 logger.error("Could not retrieve a list of documents referring to the specified authority item", e);
388 private static ArrayList<String> getRefNameServiceTypes() {
389 if (refNameServiceTypes == null) {
390 refNameServiceTypes = new ArrayList<String>();
391 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_AUTHORITY);
392 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_OBJECT);
393 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_PROCEDURE);
395 return refNameServiceTypes;
398 // Seems like a good value - no real data to set this well.
399 // Note: can set this value lower during debugging; e.g. to 3 - ADR 2012-07-10
400 private static final int N_OBJS_TO_UPDATE_PER_LOOP = 100;
402 public static int updateAuthorityRefDocs(
403 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
404 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
405 CoreSessionInterface repoSession,
408 String refPropName) throws Exception {
409 Map<String, ServiceBindingType> queriedServiceBindings = new HashMap<String, ServiceBindingType>();
410 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService = new HashMap<String, List<AuthRefConfigInfo>>();
415 int docsInCurrentPage = 0;
416 final String WHERE_CLAUSE_ADDITIONS_VALUE = null;
417 final String ORDER_BY_VALUE = CollectionSpaceClient.CORE_CREATED_AT // "collectionspace_core:createdAt";
418 + ", " + IQueryManager.NUXEO_UUID; // CSPACE-6333: Add secondary sort on uuid, in case records have the same createdAt timestamp.
420 if (repoClient instanceof RepositoryClientImpl == false) {
421 throw new InternalError("updateAuthorityRefDocs() called with unknown repoClient type!");
424 try { // REM - How can we deal with transaction and timeout issues here?
425 final int pageSize = N_OBJS_TO_UPDATE_PER_LOOP;
426 DocumentModelList docList;
427 boolean morePages = true;
430 docList = findAuthorityRefDocs(ctx, repoClient, repoSession,
431 getRefNameServiceTypes(), oldRefName, refPropName,
432 queriedServiceBindings, authRefFieldsByService, WHERE_CLAUSE_ADDITIONS_VALUE, ORDER_BY_VALUE, pageSize, currentPage, false);
434 if (docList == null) {
435 logger.debug("updateAuthorityRefDocs: no documents could be found that referenced the old refName");
438 docsInCurrentPage = docList.size();
439 logger.debug("updateAuthorityRefDocs: current page=" + currentPage + " documents included in page=" + docsInCurrentPage);
440 if (docsInCurrentPage == 0) {
441 logger.debug("updateAuthorityRefDocs: no more documents requiring refName updates could be found");
444 if (docsInCurrentPage < pageSize) {
445 logger.debug("updateAuthorityRefDocs: assuming no more documents requiring refName updates will be found, as docsInCurrentPage < pageSize");
449 // Only match complete refNames - unless and until we decide how to resolve changes
450 // to NPTs we will defer that and only change PTs or refNames as passed in.
451 int nRefsFoundThisPage = processRefObjsDocListForUpdate(docList, ctx.getTenantId(), oldRefName,
452 queriedServiceBindings, authRefFieldsByService, // Perform the refName updates on the list of document models
454 if (nRefsFoundThisPage > 0) {
455 ((RepositoryClientImpl) repoClient).saveDocListWithoutHandlerProcessing(ctx, repoSession, docList, true); // Flush the document model list out to Nuxeo storage
456 nRefsFound += nRefsFoundThisPage;
459 // FIXME: Per REM, set a limit of num objects - something like
460 // 1000K objects - and also add a log Warning after some threshold
461 docsScanned += docsInCurrentPage;
467 } catch (Exception e) {
468 logger.error("Internal error updating the AuthorityRefDocs: " + e.getLocalizedMessage());
469 logger.debug(Tools.errorToString(e, true));
472 logger.debug("updateAuthorityRefDocs replaced a total of " + nRefsFound + " authority references, within as many as " + docsScanned + " scanned document(s)");
476 private static DocumentModelList findAllAuthorityRefDocs(
477 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
478 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
479 CoreSessionInterface repoSession, List<String> serviceTypes,
482 Map<String, ServiceBindingType> queriedServiceBindings,
483 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
484 String whereClauseAdditions,
485 String orderByClause,
487 boolean computeTotal) throws DocumentException, DocumentNotFoundException {
489 return new LazyAuthorityRefDocList(ctx, repoClient, repoSession,
490 serviceTypes, refName, refPropName, queriedServiceBindings, authRefFieldsByService,
491 whereClauseAdditions, orderByClause, pageSize, computeTotal);
494 protected static DocumentModelList findAuthorityRefDocs(
495 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
496 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
497 CoreSessionInterface repoSession, List<String> serviceTypes,
500 Map<String, ServiceBindingType> queriedServiceBindings,
501 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
502 String whereClauseAdditions,
503 String orderByClause,
506 boolean computeTotal) throws DocumentException, DocumentNotFoundException {
508 // Get the service bindings for this tenant
509 TenantBindingConfigReaderImpl tReader =
510 ServiceMain.getInstance().getTenantBindingConfigReader();
511 // We need to get all the procedures, authorities, and objects.
512 List<ServiceBindingType> servicebindings = tReader.getServiceBindingsByType(ctx.getTenantId(), serviceTypes);
513 if (servicebindings == null || servicebindings.isEmpty()) {
514 logger.error("RefNameServiceUtils.getAuthorityRefDocs: No services bindings found, cannot proceed!");
517 // Filter the list for current user rights
518 servicebindings = SecurityUtils.getReadableServiceBindingsForCurrentUser(servicebindings);
520 ArrayList<String> docTypes = new ArrayList<String>();
522 String query = computeWhereClauseForAuthorityRefDocs(refName, refPropName, docTypes, servicebindings, // REM - Side effect that docTypes, authRefFieldsByService, and queriedServiceBindings get set/change. Any others?
523 queriedServiceBindings, authRefFieldsByService);
524 if (query == null) { // found no authRef fields - nothing to query
527 // Additional qualifications, like workflow state
528 if (Tools.notBlank(whereClauseAdditions)) {
529 query += " AND " + whereClauseAdditions;
531 // Now we have to issue the search
532 RepositoryClientImpl nuxeoRepoClient = (RepositoryClientImpl) repoClient;
533 DocumentWrapper<DocumentModelList> docListWrapper = nuxeoRepoClient.findDocs(ctx, repoSession,
534 docTypes, query, orderByClause, pageSize, pageNum, computeTotal);
535 // Now we gather the info for each document into the list and return
536 DocumentModelList docList = docListWrapper.getWrappedObject();
539 private static final boolean READY_FOR_COMPLEX_QUERY = true;
541 private static String computeWhereClauseForAuthorityRefDocs(
544 ArrayList<String> docTypes,
545 List<ServiceBindingType> servicebindings,
546 Map<String, ServiceBindingType> queriedServiceBindings,
547 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService) {
549 boolean fFirst = true;
550 List<String> authRefFieldPaths;
551 for (ServiceBindingType sb : servicebindings) {
552 // Gets the property names for each part, qualified with the part label (which
553 // is also the table name, the way that the repository works).
555 ServiceBindingUtils.getAllPartsPropertyValues(sb,
556 refPropName, ServiceBindingUtils.QUALIFIED_PROP_NAMES);
557 if (authRefFieldPaths.isEmpty()) {
560 ArrayList<AuthRefConfigInfo> authRefsInfo = new ArrayList<AuthRefConfigInfo>();
561 for (String spec : authRefFieldPaths) {
562 AuthRefConfigInfo arci = new AuthRefConfigInfo(spec);
563 authRefsInfo.add(arci);
566 String docType = sb.getObject().getName();
567 queriedServiceBindings.put(docType, sb);
568 authRefFieldsByService.put(docType, authRefsInfo);
569 docTypes.add(docType);
572 if (fFirst) { // found no authRef fields - nothing to query
575 // We used to build a complete matches query, but that was too complex.
576 // Just build a keyword query based upon some key pieces - the urn syntax elements and the shortID
577 // Note that this will also match the Item itself, but that will get filtered out when
578 // we compute actual matches.
579 AuthorityTermInfo authTermInfo = RefNameUtils.parseAuthorityTermInfo(refName);
581 String keywords = RefNameUtils.URN_PREFIX
582 + " AND " + (authTermInfo.inAuthority.name != null
583 ? authTermInfo.inAuthority.name : authTermInfo.inAuthority.csid)
584 + " AND " + (authTermInfo.name != null
585 ? authTermInfo.name : authTermInfo.csid); // REM - This seems likely to cause trouble. We should consider searching for the full refname -excluding the display name suffix
587 String whereClauseStr = QueryManager.createWhereClauseFromKeywords(keywords);
589 if (logger.isTraceEnabled()) {
590 logger.trace("The 'where' clause to find refObjs is: ", whereClauseStr);
593 return whereClauseStr;
596 // TODO there are multiple copies of this that should be put somewhere common.
597 protected static String getRefname(DocumentModel docModel) throws ClientException {
598 String result = (String)docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
599 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
603 private static int processRefObjsDocListForUpdate(
604 DocumentModelList docList,
607 Map<String, ServiceBindingType> queriedServiceBindings,
608 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
609 String newAuthorityRefName) {
610 return processRefObjsDocList(docList, tenantId, refName, false, queriedServiceBindings,
611 authRefFieldsByService, null, 0, 0, newAuthorityRefName);
614 private static int processRefObjsDocListForList(
615 DocumentModelList docList,
618 Map<String, ServiceBindingType> queriedServiceBindings,
619 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
620 List<AuthorityRefDocList.AuthorityRefDocItem> list,
621 int pageSize, int pageNum) {
622 return processRefObjsDocList(docList, tenantId, refName, true, queriedServiceBindings,
623 authRefFieldsByService, list, pageSize, pageNum, null);
628 * Runs through the list of found docs, processing them. If list is
629 * non-null, then processing means gather the info for items. If list is
630 * null, and newRefName is non-null, then processing means replacing and
631 * updating. If processing/updating, this must be called in the context of
632 * an open session, and caller must release Session after calling this.
635 private static int processRefObjsDocList(
636 DocumentModelList docList,
639 boolean matchBaseOnly,
640 Map<String, ServiceBindingType> queriedServiceBindings,
641 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
642 List<AuthorityRefDocList.AuthorityRefDocItem> list,
643 int pageSize, int pageNum, // Only used when constructing a list.
644 String newAuthorityRefName) {
645 UriTemplateRegistry registry = ServiceMain.getInstance().getUriTemplateRegistry();
646 Iterator<DocumentModel> iter = docList.iterator();
647 int nRefsFoundTotal = 0;
648 boolean foundSelf = false;
650 // When paginating results, we have to guess at the total. First guess is the number of docs returned
651 // by the query. However, this returns some false positives, so may be high.
652 // In addition, we can match multiple fields per doc, so this may be low. Fun, eh?
653 int nDocsReturnedInQuery = (int)docList.totalSize();
654 int nDocsProcessed = 0;
655 int firstItemInPage = pageNum*pageSize;
656 while (iter.hasNext()) {
657 DocumentModel docModel = iter.next();
658 AuthorityRefDocList.AuthorityRefDocItem ilistItem;
660 String docType = docModel.getDocumentType().getName(); // REM - This will be a tentant qualified document type
661 docType = ServiceBindingUtils.getUnqualifiedTenantDocType(docType);
662 ServiceBindingType sb = queriedServiceBindings.get(docType);
664 throw new RuntimeException(
665 "getAuthorityRefDocs: No Service Binding for docType: " + docType);
668 if (list == null) { // no list - should be update refName case.
669 if (newAuthorityRefName == null) {
670 throw new InternalError("processRefObjsDocList() called with neither an itemList nor a new RefName!");
674 firstItemInPage = 0; // Do not paginate if updating, rather than building list
675 } else { // Have a list - refObjs case
676 if (newAuthorityRefName != null) {
677 throw new InternalError("processRefObjsDocList() called with both an itemList and a new RefName!");
679 if(firstItemInPage > 100) {
680 logger.warn("Processing a large offset (size:{}, num:{}) for refObjs - will be expensive!!!",
683 // Note that we have to go through check all the fields to determine the actual page start
684 ilistItem = new AuthorityRefDocList.AuthorityRefDocItem();
685 String csid = NuxeoUtils.getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
687 String itemRefName = getRefname(docModel);
688 ilistItem.setRefName(itemRefName);
689 } catch (ClientException ce) {
690 throw new RuntimeException(
691 "processRefObjsDocList: Problem fetching refName from item Object: "
692 + ce.getLocalizedMessage());
694 ilistItem.setDocId(csid);
696 UriTemplateRegistryKey key = new UriTemplateRegistryKey(tenantId, docType);
697 StoredValuesUriTemplate template = registry.get(key);
698 if (template != null) {
699 Map<String, String> additionalValues = new HashMap<String, String>();
700 if (template.getUriTemplateType() == UriTemplateFactory.RESOURCE) {
701 additionalValues.put(UriTemplateFactory.IDENTIFIER_VAR, csid);
702 uri = template.buildUri(additionalValues);
703 } else if (template.getUriTemplateType() == UriTemplateFactory.ITEM) {
705 String inAuthorityCsid = (String) NuxeoUtils.getProperyValue(docModel, "inAuthority"); //docModel.getPropertyValue("inAuthority"); // AuthorityItemJAXBSchema.IN_AUTHORITY
706 additionalValues.put(UriTemplateFactory.IDENTIFIER_VAR, inAuthorityCsid);
707 additionalValues.put(UriTemplateFactory.ITEM_IDENTIFIER_VAR, csid);
708 uri = template.buildUri(additionalValues);
709 } catch (Exception e) {
710 logger.warn("Could not extract inAuthority property from authority item record: " + e.getMessage());
712 } else if (template.getUriTemplateType() == UriTemplateFactory.CONTACT) {
713 // FIXME: Generating contact sub-resource URIs requires additional work,
714 // as a follow-on to CSPACE-5271 - ADR 2012-08-16
715 // Sets the default (empty string) value for uri, for now
717 logger.warn("Unrecognized URI template type = " + template.getUriTemplateType());
718 // Sets the default (empty string) value for uri
720 } else { // (if template == null)
721 logger.warn("Could not retrieve URI template from registry via tenant ID "
722 + tenantId + " and docType " + docType);
723 // Sets the default (empty string) value for uri
725 ilistItem.setUri(uri);
727 ilistItem.setWorkflowState(docModel.getCurrentLifeCycleState());
728 ilistItem.setUpdatedAt(NuxeoDocumentModelHandler.getUpdatedAtAsString(docModel));
729 } catch (Exception e) {
730 logger.error("Error getting core values for doc [" + csid + "]: " + e.getLocalizedMessage());
732 ilistItem.setDocType(docType);
733 ilistItem.setDocNumber(
734 ServiceBindingUtils.getMappedFieldInDoc(sb, ServiceBindingUtils.OBJ_NUMBER_PROP, docModel));
735 ilistItem.setDocName(
736 ServiceBindingUtils.getMappedFieldInDoc(sb, ServiceBindingUtils.OBJ_NAME_PROP, docModel));
738 // Now, we have to loop over the authRefFieldsByService to figure
739 // out which field(s) matched this.
740 List<AuthRefConfigInfo> matchingAuthRefFields = authRefFieldsByService.get(docType);
741 if (matchingAuthRefFields == null || matchingAuthRefFields.isEmpty()) {
742 throw new RuntimeException(
743 "getAuthorityRefDocs: internal logic error: can't fetch authRefFields for DocType.");
745 //String authRefAncestorField = "";
746 //String authRefDescendantField = "";
747 //String sourceField = "";
749 ArrayList<RefNameServiceUtils.AuthRefInfo> foundProps = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
751 findAuthRefPropertiesInDoc(docModel, matchingAuthRefFields, refName, matchBaseOnly, foundProps); // REM - side effect that foundProps is set
752 if(!foundProps.isEmpty()) {
753 int nRefsFoundInDoc = 0;
754 for (RefNameServiceUtils.AuthRefInfo ari : foundProps) {
755 if (ilistItem != null) {
756 // So this is a true positive, and not a false one. We have to consider pagination now.
757 if(nRefsFoundTotal >= firstItemInPage) { // skipped enough already
758 if (nRefsFoundInDoc == 0) { // First one?
759 ilistItem.setSourceField(ari.getQualifiedDisplayName());
760 } else { // duplicates from one object
761 ilistItem = cloneAuthRefDocItem(ilistItem, ari.getQualifiedDisplayName());
764 nRefsFoundInDoc++; // Only increment if processed, or clone logic above will fail
766 } else { // update refName case
767 Property propToUpdate = ari.getProperty();
768 propToUpdate.setValue(newAuthorityRefName);
770 nRefsFoundTotal++; // Whether we processed or not, we found - essential to pagination logic
772 } else if(ilistItem != null) {
773 String docRefName = ilistItem.getRefName();
775 (docRefName!=null && docRefName.startsWith(refName))
776 :refName.equals(docRefName)) {
777 // We found the self for an item
779 logger.debug("getAuthorityRefDocs: Result: "
780 + docType + " [" + NuxeoUtils.getCsid(docModel)
781 + "] appears to be self for: ["
784 logger.debug("getAuthorityRefDocs: Result: "
785 + docType + " [" + NuxeoUtils.getCsid(docModel)
786 + "] does not reference ["
790 } catch (ClientException ce) {
791 throw new RuntimeException(
792 "getAuthorityRefDocs: Problem fetching values from repo: " + ce.getLocalizedMessage());
795 // Done processing that doc. Are we done with the whole page?
796 // Note pageSize <=0 means do them all
797 if((pageSize > 0) && ((nRefsFoundTotal-firstItemInPage)>=pageSize)) {
798 // Quitting early, so we need to estimate the total. Assume one per doc
799 // for the rest of the docs we matched in the query
800 int unprocessedDocs = nDocsReturnedInQuery - nDocsProcessed;
801 if(unprocessedDocs>0) {
802 // We generally match ourselves in the keyword search. If we already saw ourselves
803 // then do not try to correct for this. Otherwise, decrement the total.
804 // Yes, this is fairly goofy, but the whole estimation mechanism is goofy.
807 nRefsFoundTotal += unprocessedDocs;
811 } // close while(iterator)
812 return nRefsFoundTotal;
815 private static AuthorityRefDocList.AuthorityRefDocItem cloneAuthRefDocItem(
816 AuthorityRefDocList.AuthorityRefDocItem ilistItem, String sourceField) {
817 AuthorityRefDocList.AuthorityRefDocItem newlistItem = new AuthorityRefDocList.AuthorityRefDocItem();
818 newlistItem.setDocId(ilistItem.getDocId());
819 newlistItem.setDocName(ilistItem.getDocName());
820 newlistItem.setDocNumber(ilistItem.getDocNumber());
821 newlistItem.setDocType(ilistItem.getDocType());
822 newlistItem.setUri(ilistItem.getUri());
823 newlistItem.setSourceField(sourceField);
827 public static List<AuthRefInfo> findAuthRefPropertiesInDoc(
828 DocumentModel docModel,
829 List<AuthRefConfigInfo> authRefFieldInfo,
830 String refNameToMatch,
831 List<AuthRefInfo> foundProps) {
832 return findAuthRefPropertiesInDoc(docModel, authRefFieldInfo,
833 refNameToMatch, false, foundProps);
836 public static List<AuthRefInfo> findAuthRefPropertiesInDoc(
837 DocumentModel docModel,
838 List<AuthRefConfigInfo> authRefFieldInfo,
839 String refNameToMatch,
840 boolean matchBaseOnly,
841 List<AuthRefInfo> foundProps) {
842 // Assume that authRefFieldInfo is keyed by the field name (possibly mapped for UI)
843 // and the values are elPaths to the field, where intervening group structures in
844 // lists of complex structures are replaced with "*". Thus, valid paths include
845 // the following (note that the ServiceBindingUtils prepend schema names to configured values):
846 // "schemaname:fieldname"
847 // "schemaname:scalarlistname"
848 // "schemaname:complexfieldname/fieldname"
849 // "schemaname:complexlistname/*/fieldname"
850 // "schemaname:complexlistname/*/scalarlistname"
851 // "schemaname:complexlistname/*/complexfieldname/fieldname"
852 // "schemaname:complexlistname/*/complexlistname/*/fieldname"
854 for (AuthRefConfigInfo arci : authRefFieldInfo) {
856 // Get first property and work down as needed.
857 Property prop = docModel.getProperty(arci.pathEls[0]);
858 findAuthRefPropertiesInProperty(foundProps, prop, arci, 0, refNameToMatch, matchBaseOnly);
859 } catch (Exception e) {
860 logger.error("Problem fetching property: " + arci.pathEls[0]);
866 private static List<AuthRefInfo> findAuthRefPropertiesInProperty(
867 List<AuthRefInfo> foundProps,
869 AuthRefConfigInfo arci,
870 int pathStartIndex, // Supports recursion and we work down the path
871 String refNameToMatch,
872 boolean matchBaseOnly ) {
873 if (pathStartIndex >= arci.pathEls.length) {
874 throw new ArrayIndexOutOfBoundsException("Index = " + pathStartIndex + " for path: "
875 + arci.pathEls.toString());
877 AuthRefInfo ari = null;
882 if (prop instanceof StringProperty) { // scalar string
883 addARIifMatches(refNameToMatch, matchBaseOnly, arci, prop, foundProps); // REM - Side effect that foundProps gets changed/updated
884 } else if (prop instanceof List) {
885 List<Property> propList = (List<Property>) prop;
886 // run through list. Must either be list of Strings, or Complex
887 for (Property listItemProp : propList) {
888 if (listItemProp instanceof StringProperty) {
889 if (arci.pathEls.length - pathStartIndex != 1) {
890 logger.error("Configuration for authRefs does not match schema structure: "
891 + arci.pathEls.toString());
894 addARIifMatches(refNameToMatch, matchBaseOnly, arci, listItemProp, foundProps);
896 } else if (listItemProp.isComplex()) {
897 // Just recurse to handle this. Note that since this is a list of complex,
898 // which should look like listName/*/... we add 2 to the path start index
899 findAuthRefPropertiesInProperty(foundProps, listItemProp, arci,
900 pathStartIndex + 2, refNameToMatch, matchBaseOnly);
902 logger.error("Configuration for authRefs does not match schema structure: "
903 + arci.pathEls.toString());
907 } else if (prop.isComplex()) {
908 String localPropName = arci.pathEls[pathStartIndex];
910 Property localProp = prop.get(localPropName);
911 // Now just recurse, pushing down the path 1 step
912 findAuthRefPropertiesInProperty(foundProps, localProp, arci,
913 pathStartIndex, refNameToMatch, matchBaseOnly);
914 } catch (PropertyNotFoundException pnfe) {
915 logger.error("Could not find property: [" + localPropName + "] in path: "
916 + arci.getFullPath());
917 // Fall through - ari will be null and we will continue...
920 logger.error("Configuration for authRefs does not match schema structure: "
921 + arci.pathEls.toString());
925 foundProps.add(ari); //FIXME: REM - This is dead code. 'ari' is never touched after being initalized to null. Why?
931 private static void addARIifMatches(
932 String refNameToMatch,
933 boolean matchBaseOnly,
934 AuthRefConfigInfo arci,
936 List<AuthRefInfo> foundProps) {
937 // Need to either match a passed refName
938 // OR have no refName to match but be non-empty
940 String value = (String) prop.getValue();
941 if (((refNameToMatch != null) &&
943 (value!=null && value.startsWith(refNameToMatch))
944 :refNameToMatch.equals(value)))
945 || ((refNameToMatch == null) && Tools.notBlank(value))) {
947 logger.debug("Found a match on property: " + prop.getPath() + " with value: [" + value + "]");
948 AuthRefInfo ari = new AuthRefInfo(arci, prop);
951 } catch (PropertyException pe) {
952 logger.debug("PropertyException on: " + prop.getPath() + pe.getLocalizedMessage());
956 public static String buildWhereForAuthByName(String authorityCommonSchemaName, String name) {
957 return authorityCommonSchemaName
958 + ":" + AuthorityJAXBSchema.SHORT_IDENTIFIER
962 public static String buildWhereForAuthItemByName(String authorityItemCommonSchemaName, String name, String parentcsid) {
963 return authorityItemCommonSchemaName
964 + ":" + AuthorityItemJAXBSchema.SHORT_IDENTIFIER
965 + "='" + name + "' AND "
966 + authorityItemCommonSchemaName + ":"
967 + AuthorityItemJAXBSchema.IN_AUTHORITY + "="
968 + "'" + parentcsid + "'";
972 * Identifies whether the refName was found in the supplied field. If passed
973 * a new RefName, will set that into fields in which the old one was found.
975 * Only works for: * Scalar fields * Repeatable scalar fields (aka
976 * multi-valued fields)
978 * Does not work for: * Structured fields (complexTypes) * Repeatable
979 * structured fields (repeatable complexTypes) private static int
980 * refNameFoundInField(String oldRefName, Property fieldValue, String
981 * newRefName) { int nFound = 0; if (fieldValue instanceof List) {
982 * List<Property> fieldValueList = (List) fieldValue; for (Property
983 * listItemValue : fieldValueList) { try { if ((listItemValue instanceof
985 * oldRefName.equalsIgnoreCase((String)listItemValue.getValue())) {
986 * nFound++; if(newRefName!=null) { fieldValue.setValue(newRefName); } else
987 * { // We cannot quit after the first, if we are replacing values. // If we
988 * are just looking (not replacing), finding one is enough. break; } } }
989 * catch( PropertyException pe ) {} } } else { try { if ((fieldValue
990 * instanceof StringProperty) &&
991 * oldRefName.equalsIgnoreCase((String)fieldValue.getValue())) { nFound++;
992 * if(newRefName!=null) { fieldValue.setValue(newRefName); } } } catch(
993 * PropertyException pe ) {} } return nFound; }