2 * This document is a part of the source code and related artifacts for
\r
3 * CollectionSpace, an open source collections management system for museums and
\r
4 * related institutions:
\r
6 * http://www.collectionspace.org http://wiki.collectionspace.org
\r
8 * Copyright 2009 University of California at Berkeley
\r
10 * Licensed under the Educational Community License (ECL), Version 2.0. You may
\r
11 * not use this file except in compliance with this License.
\r
13 * You may obtain a copy of the ECL 2.0 License at
\r
15 * https://source.collectionspace.org/collection-space/LICENSE.txt
\r
17 * Unless required by applicable law or agreed to in writing, software
\r
18 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
\r
19 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
\r
20 * License for the specific language governing permissions and limitations under
\r
23 package org.collectionspace.services.common.vocabulary;
\r
25 import java.util.ArrayList;
\r
26 import java.util.HashMap;
\r
27 import java.util.Iterator;
\r
28 import java.util.List;
\r
29 import java.util.Map;
\r
31 import org.nuxeo.ecm.core.api.ClientException;
\r
32 import org.nuxeo.ecm.core.api.DocumentModel;
\r
33 import org.nuxeo.ecm.core.api.DocumentModelList;
\r
34 import org.nuxeo.ecm.core.api.model.Property;
\r
35 import org.nuxeo.ecm.core.api.model.PropertyException;
\r
36 import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
\r
37 import org.nuxeo.ecm.core.api.model.impl.primitives.StringProperty;
\r
38 import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
\r
40 import org.slf4j.Logger;
\r
41 import org.slf4j.LoggerFactory;
\r
43 import org.collectionspace.services.client.CollectionSpaceClient;
\r
44 import org.collectionspace.services.client.IQueryManager;
\r
45 import org.collectionspace.services.client.IRelationsManager;
\r
46 import org.collectionspace.services.client.PoxPayloadIn;
\r
47 import org.collectionspace.services.client.PoxPayloadOut;
\r
48 import org.collectionspace.services.common.ServiceMain;
\r
49 import org.collectionspace.services.common.StoredValuesUriTemplate;
\r
50 import org.collectionspace.services.common.UriTemplateFactory;
\r
51 import org.collectionspace.services.common.UriTemplateRegistry;
\r
52 import org.collectionspace.services.common.UriTemplateRegistryKey;
\r
53 import org.collectionspace.services.common.context.ServiceContext;
\r
54 import org.collectionspace.services.common.context.AbstractServiceContextImpl;
\r
55 import org.collectionspace.services.common.api.RefNameUtils;
\r
56 import org.collectionspace.services.common.api.Tools;
\r
57 import org.collectionspace.services.common.api.RefNameUtils.AuthorityTermInfo;
\r
58 import org.collectionspace.services.common.authorityref.AuthorityRefDocList;
\r
59 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
\r
60 import org.collectionspace.services.common.context.ServiceBindingUtils;
\r
61 import org.collectionspace.services.common.document.DocumentException;
\r
62 import org.collectionspace.services.common.document.DocumentFilter;
\r
63 import org.collectionspace.services.common.document.DocumentNotFoundException;
\r
64 import org.collectionspace.services.common.document.DocumentUtils;
\r
65 import org.collectionspace.services.common.document.DocumentWrapper;
\r
66 import org.collectionspace.services.common.query.QueryManager;
\r
67 import org.collectionspace.services.common.relation.RelationUtils;
\r
68 import org.collectionspace.services.common.repository.RepositoryClient;
\r
69 import org.collectionspace.services.nuxeo.client.java.DocHandlerBase;
\r
70 import org.collectionspace.services.nuxeo.client.java.RepositoryInstanceInterface;
\r
71 import org.collectionspace.services.nuxeo.client.java.RepositoryJavaClientImpl;
\r
72 import org.collectionspace.services.common.security.SecurityUtils;
\r
73 import org.collectionspace.services.config.service.ServiceBindingType;
\r
74 import org.collectionspace.services.jaxb.AbstractCommonList;
\r
75 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
\r
78 * RefNameServiceUtils is a collection of services utilities related to refName
\r
81 * $LastChangedRevision: $ $LastChangedDate: $
\r
83 public class RefNameServiceUtils {
\r
85 public static class AuthRefConfigInfo {
\r
87 public String getQualifiedDisplayName() {
\r
88 return (Tools.isBlank(schema))
\r
89 ? displayName : DocumentUtils.appendSchemaName(schema, displayName);
\r
92 public String getDisplayName() {
\r
96 public void setDisplayName(String displayName) {
\r
97 this.displayName = displayName;
\r
102 public String getSchema() {
\r
106 public void setSchema(String schema) {
\r
107 this.schema = schema;
\r
110 public String getFullPath() {
\r
114 public void setFullPath(String fullPath) {
\r
115 this.fullPath = fullPath;
\r
118 protected String[] pathEls;
\r
120 public AuthRefConfigInfo(AuthRefConfigInfo arci) {
\r
121 this.displayName = arci.displayName;
\r
122 this.schema = arci.schema;
\r
123 this.fullPath = arci.fullPath;
\r
124 this.pathEls = arci.pathEls;
\r
125 // Skip the pathElse check, since we are creatign from another (presumably valid) arci.
\r
128 public AuthRefConfigInfo(String displayName, String schema, String fullPath, String[] pathEls) {
\r
129 this.displayName = displayName;
\r
130 this.schema = schema;
\r
131 this.fullPath = fullPath;
\r
132 this.pathEls = pathEls;
\r
136 // Split a config value string like "intakes_common:collector", or
\r
137 // "collectionobjects_common:contentPeoples|contentPeople"
\r
138 // "collectionobjects_common:assocEventGroupList/*/assocEventPlace"
\r
139 // If has a pipe ('|') second part is a displayLabel, and first is path
\r
140 // Otherwise, entry is a path, and can use the last pathElement as displayName
\r
141 // Should be schema qualified.
\r
142 public AuthRefConfigInfo(String configString) {
\r
143 String[] pair = configString.split("\\|", 2);
\r
145 String displayName, fullPath;
\r
146 if (pair.length == 1) {
\r
147 // no label specifier, so we'll defer getting label
\r
148 fullPath = pair[0];
\r
149 pathEls = pair[0].split("/");
\r
150 displayName = pathEls[pathEls.length - 1];
\r
152 fullPath = pair[0];
\r
153 pathEls = pair[0].split("/");
\r
154 displayName = pair[1];
\r
156 String[] schemaSplit = pathEls[0].split(":", 2);
\r
158 if (schemaSplit.length == 1) { // schema not specified
\r
161 schema = schemaSplit[0];
\r
162 if (pair.length == 1 && pathEls.length == 1) { // simplest case of field in top level schema, no labelll
\r
163 displayName = schemaSplit[1]; // Have to fix up displayName to have no schema
\r
166 this.displayName = displayName;
\r
167 this.schema = schema;
\r
168 this.fullPath = fullPath;
\r
169 this.pathEls = pathEls;
\r
173 protected void checkPathEls() {
\r
174 int len = pathEls.length;
\r
176 throw new InternalError("Bad values in authRef info - caller screwed up:" + fullPath);
\r
178 // Handle case of them putting a leading slash on the path
\r
179 if (len > 1 && pathEls[0].endsWith(":")) {
\r
181 String[] newArray = new String[len];
\r
182 newArray[0] = pathEls[0] + pathEls[1];
\r
184 System.arraycopy(pathEls, 2, newArray, 1, len - 1);
\r
186 pathEls = newArray;
\r
191 public static class AuthRefInfo extends AuthRefConfigInfo {
\r
193 public Property getProperty() {
\r
197 public void setProperty(Property property) {
\r
198 this.property = property;
\r
202 public AuthRefInfo(String displayName, String schema, String fullPath, String[] pathEls, Property prop) {
\r
203 super(displayName, schema, fullPath, pathEls);
\r
204 this.property = prop;
\r
207 public AuthRefInfo(AuthRefConfigInfo arci, Property prop) {
\r
209 this.property = prop;
\r
213 private static final Logger logger = LoggerFactory.getLogger(RefNameServiceUtils.class);
\r
214 private static ArrayList<String> refNameServiceTypes = null;
\r
216 public static void updateRefNamesInRelations(
\r
217 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
\r
218 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
\r
219 RepositoryInstanceInterface repoSession,
\r
221 String newRefName) throws Exception {
\r
223 // First, look for and update all the places where the refName is the "subject" of the relationship
\r
225 RelationUtils.updateRefNamesInRelations(ctx, repoClient, repoSession, IRelationsManager.SUBJECT_REFNAME, oldRefName, newRefName);
\r
228 // Next, look for and update all the places where the refName is the "object" of the relationship
\r
230 RelationUtils.updateRefNamesInRelations(ctx, repoClient, repoSession, IRelationsManager.OBJECT_REFNAME, oldRefName, newRefName);
\r
233 public static List<AuthRefConfigInfo> getConfiguredAuthorityRefs(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
\r
234 List<String> authRefFields =
\r
235 ((AbstractServiceContextImpl) ctx).getAllPartsPropertyValues(
\r
236 ServiceBindingUtils.AUTH_REF_PROP, ServiceBindingUtils.QUALIFIED_PROP_NAMES);
\r
237 ArrayList<AuthRefConfigInfo> authRefsInfo = new ArrayList<AuthRefConfigInfo>(authRefFields.size());
\r
238 for (String spec : authRefFields) {
\r
239 AuthRefConfigInfo arci = new AuthRefConfigInfo(spec);
\r
240 authRefsInfo.add(arci);
\r
242 return authRefsInfo;
\r
245 public static AuthorityRefDocList getAuthorityRefDocs(
\r
246 RepositoryInstanceInterface repoSession,
\r
247 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
\r
248 UriTemplateRegistry uriTemplateRegistry,
\r
249 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
\r
250 List<String> serviceTypes,
\r
252 String refPropName, // authRef or termRef, authorities or vocab terms.
\r
253 DocumentFilter filter, boolean computeTotal)
\r
254 throws DocumentException, DocumentNotFoundException {
\r
255 AuthorityRefDocList wrapperList = new AuthorityRefDocList();
\r
256 AbstractCommonList commonList = (AbstractCommonList) wrapperList;
\r
257 int pageNum = filter.getStartPage();
\r
258 int pageSize = filter.getPageSize();
\r
260 List<AuthorityRefDocList.AuthorityRefDocItem> list =
\r
261 wrapperList.getAuthorityRefDocItem();
\r
263 Map<String, ServiceBindingType> queriedServiceBindings = new HashMap<String, ServiceBindingType>();
\r
264 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService = new HashMap<String, List<AuthRefConfigInfo>>();
\r
266 RepositoryJavaClientImpl nuxeoRepoClient = (RepositoryJavaClientImpl) repoClient;
\r
268 // Ignore any provided page size and number query parameters in
\r
269 // the following call, as they pertain to the list of authority
\r
270 // references to be returned, not to the list of documents to be
\r
271 // scanned for those references.
\r
273 // Get a list of possibly referencing documents. This list is
\r
274 // lazily loaded, page by page. Ideally, only one page will
\r
275 // need to be loaded to fill one page of results. Some number
\r
276 // of possibly referencing documents will be false positives,
\r
277 // so use a page size of double the requested page size to
\r
278 // account for those.
\r
279 DocumentModelList docList = findAllAuthorityRefDocs(ctx, repoClient, repoSession,
\r
280 serviceTypes, refName, refPropName, queriedServiceBindings, authRefFieldsByService,
\r
281 filter.getWhereClause(), null, 2*pageSize, computeTotal);
\r
283 if (docList == null) { // found no authRef fields - nothing to process
\r
284 return wrapperList;
\r
287 // set the fieldsReturned list. Even though this is a fixed schema, app layer treats
\r
288 // this like other abstract common lists
\r
290 * <xs:element name="docType" type="xs:string" minOccurs="1" />
\r
291 * <xs:element name="docId" type="xs:string" minOccurs="1" />
\r
292 * <xs:element name="docNumber" type="xs:string" minOccurs="0" />
\r
293 * <xs:element name="docName" type="xs:string" minOccurs="0" />
\r
294 * <xs:element name="sourceField" type="xs:string" minOccurs="1" />
\r
295 * <xs:element name="uri" type="xs:anyURI" minOccurs="1" />
\r
296 * <xs:element name="refName" type="xs:String" minOccurs="1" />
\r
297 * <xs:element name="updatedAt" type="xs:string" minOccurs="1" />
\r
298 * <xs:element name="workflowState" type="xs:string" minOccurs="1"
\r
301 String fieldList = "docType|docId|docNumber|docName|sourceField|uri|refName|updatedAt|workflowState";
\r
302 commonList.setFieldsReturned(fieldList);
\r
304 // As a side-effect, the method called below modifies the value of
\r
305 // the 'list' variable, which holds the list of references to
\r
306 // an authority item.
\r
308 // There can be more than one reference to a particular authority
\r
309 // item within any individual document scanned, so the number of
\r
310 // authority references may potentially exceed the total number
\r
311 // of documents scanned.
\r
313 // Strip off displayName and only match the base, so we get references to all
\r
314 // the NPTs as well as the PT.
\r
315 String strippedRefName = RefNameUtils.stripAuthorityTermDisplayName(refName);
\r
317 // *** Need to pass in pagination info here.
\r
318 int nRefsFound = processRefObjsDocListForList(docList, ctx.getTenantId(), strippedRefName,
\r
319 queriedServiceBindings, authRefFieldsByService, // the actual list size needs to be updated to the size of "list"
\r
320 list, pageSize, pageNum);
\r
322 commonList.setPageSize(pageSize);
\r
324 // Values returned in the pagination block above the list items
\r
325 // need to reflect the number of references to authority items
\r
326 // returned, rather than the number of documents originally scanned
\r
327 // to find such references.
\r
328 // This will be an estimate only...
\r
329 commonList.setPageNum(pageNum);
\r
330 commonList.setTotalItems(nRefsFound); // Accurate if total was scanned, otherwise, just an estimate
\r
331 commonList.setItemsInPage(list.size());
\r
333 /* Pagination is now handled in the processing step
\r
334 // Slice the list to return only the specified page of items
\r
335 // in the list results.
\r
337 // FIXME: There may well be a pattern-based way to do this
\r
338 // in our framework, and if we can eliminate much of the
\r
339 // non-DRY code below, that would be desirable.
\r
341 int startIndex = 0;
\r
344 // Return all results if pageSize is 0.
\r
345 if (pageSize == 0) {
\r
347 endIndex = list.size();
\r
349 startIndex = pageNum * pageSize;
\r
352 // Return an empty list when the start of the requested page is
\r
353 // beyond the last item in the list.
\r
354 if (startIndex > list.size()) {
\r
355 wrapperList.getAuthorityRefDocItem().clear();
\r
356 commonList.setItemsInPage(wrapperList.getAuthorityRefDocItem().size());
\r
357 return wrapperList;
\r
360 // Otherwise, return a list of items from the start of the specified
\r
361 // page through the last item on that page, or otherwise through the
\r
362 // last item in the entire list, if that occurs earlier than the end
\r
363 // of the specified page.
\r
364 if (endIndex == 0) {
\r
365 int pageEndIndex = ((startIndex + pageSize));
\r
366 endIndex = (pageEndIndex > list.size()) ? list.size() : pageEndIndex;
\r
369 // Slice the list to return only the specified page of results.
\r
370 // Note: the second argument to List.subList(), endIndex, is
\r
371 // exclusive of the item at its index position, reflecting the
\r
372 // zero-index nature of the list.
\r
373 List<AuthorityRefDocList.AuthorityRefDocItem> currentPageList =
\r
374 new ArrayList<AuthorityRefDocList.AuthorityRefDocItem>(list.subList(startIndex, endIndex));
\r
375 wrapperList.getAuthorityRefDocItem().clear();
\r
376 wrapperList.getAuthorityRefDocItem().addAll(currentPageList);
\r
377 commonList.setItemsInPage(currentPageList.size());
\r
380 if (logger.isDebugEnabled() && (nRefsFound < docList.size())) {
\r
381 logger.debug("Internal curiosity: got fewer matches of refs than # docs matched..."); // We found a ref to ourself and have excluded it.
\r
383 } catch (Exception e) {
\r
384 logger.error("Could not retrieve a list of documents referring to the specified authority item", e);
\r
385 wrapperList = null;
\r
388 return wrapperList;
\r
391 private static ArrayList<String> getRefNameServiceTypes() {
\r
392 if (refNameServiceTypes == null) {
\r
393 refNameServiceTypes = new ArrayList<String>();
\r
394 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_AUTHORITY);
\r
395 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_OBJECT);
\r
396 refNameServiceTypes.add(ServiceBindingUtils.SERVICE_TYPE_PROCEDURE);
\r
398 return refNameServiceTypes;
\r
401 // Seems like a good value - no real data to set this well.
\r
402 // Note: can set this value lower during debugging; e.g. to 3 - ADR 2012-07-10
\r
403 private static final int N_OBJS_TO_UPDATE_PER_LOOP = 100;
\r
405 public static int updateAuthorityRefDocs(
\r
406 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
\r
407 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
\r
408 RepositoryInstanceInterface repoSession,
\r
411 String refPropName) throws Exception {
\r
412 Map<String, ServiceBindingType> queriedServiceBindings = new HashMap<String, ServiceBindingType>();
\r
413 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService = new HashMap<String, List<AuthRefConfigInfo>>();
\r
415 int docsScanned = 0;
\r
416 int nRefsFound = 0;
\r
417 int currentPage = 0;
\r
418 int docsInCurrentPage = 0;
\r
419 final String WHERE_CLAUSE_ADDITIONS_VALUE = null;
\r
420 final String ORDER_BY_VALUE = CollectionSpaceClient.CORE_CREATED_AT // "collectionspace_core:createdAt";
\r
421 + ", " + IQueryManager.NUXEO_UUID; // CSPACE-6333: Add secondary sort on uuid, in case records have the same createdAt timestamp.
\r
423 if (repoClient instanceof RepositoryJavaClientImpl == false) {
\r
424 throw new InternalError("updateAuthorityRefDocs() called with unknown repoClient type!");
\r
427 try { // REM - How can we deal with transaction and timeout issues here?
\r
428 final int pageSize = N_OBJS_TO_UPDATE_PER_LOOP;
\r
429 DocumentModelList docList;
\r
430 boolean morePages = true;
\r
431 while (morePages) {
\r
433 docList = findAuthorityRefDocs(ctx, repoClient, repoSession,
\r
434 getRefNameServiceTypes(), oldRefName, refPropName,
\r
435 queriedServiceBindings, authRefFieldsByService, WHERE_CLAUSE_ADDITIONS_VALUE, ORDER_BY_VALUE, pageSize, currentPage, false);
\r
437 if (docList == null) {
\r
438 logger.debug("updateAuthorityRefDocs: no documents could be found that referenced the old refName");
\r
441 docsInCurrentPage = docList.size();
\r
442 logger.debug("updateAuthorityRefDocs: current page=" + currentPage + " documents included in page=" + docsInCurrentPage);
\r
443 if (docsInCurrentPage == 0) {
\r
444 logger.debug("updateAuthorityRefDocs: no more documents requiring refName updates could be found");
\r
447 if (docsInCurrentPage < pageSize) {
\r
448 logger.debug("updateAuthorityRefDocs: assuming no more documents requiring refName updates will be found, as docsInCurrentPage < pageSize");
\r
452 // Only match complete refNames - unless and until we decide how to resolve changes
\r
453 // to NPTs we will defer that and only change PTs or refNames as passed in.
\r
454 int nRefsFoundThisPage = processRefObjsDocListForUpdate(docList, ctx.getTenantId(), oldRefName,
\r
455 queriedServiceBindings, authRefFieldsByService, // Perform the refName updates on the list of document models
\r
457 if (nRefsFoundThisPage > 0) {
\r
458 ((RepositoryJavaClientImpl) repoClient).saveDocListWithoutHandlerProcessing(ctx, repoSession, docList, true); // Flush the document model list out to Nuxeo storage
\r
459 nRefsFound += nRefsFoundThisPage;
\r
462 // FIXME: Per REM, set a limit of num objects - something like
\r
463 // 1000K objects - and also add a log Warning after some threshold
\r
464 docsScanned += docsInCurrentPage;
\r
470 } catch (Exception e) {
\r
471 logger.error("Internal error updating the AuthorityRefDocs: " + e.getLocalizedMessage());
\r
472 logger.debug(Tools.errorToString(e, true));
\r
475 logger.debug("updateAuthorityRefDocs replaced a total of " + nRefsFound + " authority references, within as many as " + docsScanned + " scanned document(s)");
\r
479 private static DocumentModelList findAllAuthorityRefDocs(
\r
480 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
\r
481 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
\r
482 RepositoryInstanceInterface repoSession, List<String> serviceTypes,
\r
484 String refPropName,
\r
485 Map<String, ServiceBindingType> queriedServiceBindings,
\r
486 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
\r
487 String whereClauseAdditions,
\r
488 String orderByClause,
\r
490 boolean computeTotal) throws DocumentException, DocumentNotFoundException {
\r
492 return new LazyAuthorityRefDocList(ctx, repoClient, repoSession,
\r
493 serviceTypes, refName, refPropName, queriedServiceBindings, authRefFieldsByService,
\r
494 whereClauseAdditions, orderByClause, pageSize, computeTotal);
\r
497 protected static DocumentModelList findAuthorityRefDocs(
\r
498 ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
\r
499 RepositoryClient<PoxPayloadIn, PoxPayloadOut> repoClient,
\r
500 RepositoryInstanceInterface repoSession, List<String> serviceTypes,
\r
502 String refPropName,
\r
503 Map<String, ServiceBindingType> queriedServiceBindings,
\r
504 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
\r
505 String whereClauseAdditions,
\r
506 String orderByClause,
\r
509 boolean computeTotal) throws DocumentException, DocumentNotFoundException {
\r
511 // Get the service bindings for this tenant
\r
512 TenantBindingConfigReaderImpl tReader =
\r
513 ServiceMain.getInstance().getTenantBindingConfigReader();
\r
514 // We need to get all the procedures, authorities, and objects.
\r
515 List<ServiceBindingType> servicebindings = tReader.getServiceBindingsByType(ctx.getTenantId(), serviceTypes);
\r
516 if (servicebindings == null || servicebindings.isEmpty()) {
\r
517 logger.error("RefNameServiceUtils.getAuthorityRefDocs: No services bindings found, cannot proceed!");
\r
520 // Filter the list for current user rights
\r
521 servicebindings = SecurityUtils.getReadableServiceBindingsForCurrentUser(servicebindings);
\r
523 ArrayList<String> docTypes = new ArrayList<String>();
\r
525 String query = computeWhereClauseForAuthorityRefDocs(refName, refPropName, docTypes, servicebindings, // REM - Side effect that docTypes array gets set. Any others?
\r
526 queriedServiceBindings, authRefFieldsByService);
\r
527 if (query == null) { // found no authRef fields - nothing to query
\r
530 // Additional qualifications, like workflow state
\r
531 if (Tools.notBlank(whereClauseAdditions)) {
\r
532 query += " AND " + whereClauseAdditions;
\r
534 // Now we have to issue the search
\r
535 RepositoryJavaClientImpl nuxeoRepoClient = (RepositoryJavaClientImpl) repoClient;
\r
536 DocumentWrapper<DocumentModelList> docListWrapper = nuxeoRepoClient.findDocs(ctx, repoSession,
\r
537 docTypes, query, orderByClause, pageSize, pageNum, computeTotal);
\r
538 // Now we gather the info for each document into the list and return
\r
539 DocumentModelList docList = docListWrapper.getWrappedObject();
\r
542 private static final boolean READY_FOR_COMPLEX_QUERY = true;
\r
544 private static String computeWhereClauseForAuthorityRefDocs(
\r
546 String refPropName,
\r
547 ArrayList<String> docTypes,
\r
548 List<ServiceBindingType> servicebindings,
\r
549 Map<String, ServiceBindingType> queriedServiceBindings,
\r
550 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService) {
\r
552 boolean fFirst = true;
\r
553 List<String> authRefFieldPaths;
\r
554 for (ServiceBindingType sb : servicebindings) {
\r
555 // Gets the property names for each part, qualified with the part label (which
\r
556 // is also the table name, the way that the repository works).
\r
557 authRefFieldPaths =
\r
558 ServiceBindingUtils.getAllPartsPropertyValues(sb,
\r
559 refPropName, ServiceBindingUtils.QUALIFIED_PROP_NAMES);
\r
560 if (authRefFieldPaths.isEmpty()) {
\r
563 ArrayList<AuthRefConfigInfo> authRefsInfo = new ArrayList<AuthRefConfigInfo>();
\r
564 for (String spec : authRefFieldPaths) {
\r
565 AuthRefConfigInfo arci = new AuthRefConfigInfo(spec);
\r
566 authRefsInfo.add(arci);
\r
569 String docType = sb.getObject().getName();
\r
570 queriedServiceBindings.put(docType, sb);
\r
571 authRefFieldsByService.put(docType, authRefsInfo);
\r
572 docTypes.add(docType);
\r
575 if (fFirst) { // found no authRef fields - nothing to query
\r
578 // We used to build a complete matches query, but that was too complex.
\r
579 // Just build a keyword query based upon some key pieces - the urn syntax elements and the shortID
\r
580 // Note that this will also match the Item itself, but that will get filtered out when
\r
581 // we compute actual matches.
\r
582 AuthorityTermInfo authTermInfo = RefNameUtils.parseAuthorityTermInfo(refName);
\r
584 String keywords = RefNameUtils.URN_PREFIX
\r
585 + " AND " + (authTermInfo.inAuthority.name != null
\r
586 ? authTermInfo.inAuthority.name : authTermInfo.inAuthority.csid)
\r
587 + " AND " + (authTermInfo.name != null
\r
588 ? authTermInfo.name : authTermInfo.csid);
\r
590 String whereClauseStr = QueryManager.createWhereClauseFromKeywords(keywords);
\r
592 if (logger.isTraceEnabled()) {
\r
593 logger.trace("The 'where' clause to find refObjs is: ", whereClauseStr);
\r
596 return whereClauseStr;
\r
599 // TODO there are multiple copies of this that should be put somewhere common.
\r
600 protected static String getRefname(DocumentModel docModel) throws ClientException {
\r
601 String result = (String)docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
\r
602 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
\r
606 private static int processRefObjsDocListForUpdate(
\r
607 DocumentModelList docList,
\r
610 Map<String, ServiceBindingType> queriedServiceBindings,
\r
611 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
\r
612 String newAuthorityRefName) {
\r
613 return processRefObjsDocList(docList, tenantId, refName, false, queriedServiceBindings,
\r
614 authRefFieldsByService, null, 0, 0, newAuthorityRefName);
\r
617 private static int processRefObjsDocListForList(
\r
618 DocumentModelList docList,
\r
621 Map<String, ServiceBindingType> queriedServiceBindings,
\r
622 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
\r
623 List<AuthorityRefDocList.AuthorityRefDocItem> list,
\r
624 int pageSize, int pageNum) {
\r
625 return processRefObjsDocList(docList, tenantId, refName, true, queriedServiceBindings,
\r
626 authRefFieldsByService, list, pageSize, pageNum, null);
\r
631 * Runs through the list of found docs, processing them. If list is
\r
632 * non-null, then processing means gather the info for items. If list is
\r
633 * null, and newRefName is non-null, then processing means replacing and
\r
634 * updating. If processing/updating, this must be called in the context of
\r
635 * an open session, and caller must release Session after calling this.
\r
638 private static int processRefObjsDocList(
\r
639 DocumentModelList docList,
\r
642 boolean matchBaseOnly,
\r
643 Map<String, ServiceBindingType> queriedServiceBindings,
\r
644 Map<String, List<AuthRefConfigInfo>> authRefFieldsByService,
\r
645 List<AuthorityRefDocList.AuthorityRefDocItem> list,
\r
646 int pageSize, int pageNum, // Only used when constructing a list.
\r
647 String newAuthorityRefName) {
\r
648 UriTemplateRegistry registry = ServiceMain.getInstance().getUriTemplateRegistry();
\r
649 Iterator<DocumentModel> iter = docList.iterator();
\r
650 int nRefsFoundTotal = 0;
\r
651 boolean foundSelf = false;
\r
653 // When paginating results, we have to guess at the total. First guess is the number of docs returned
\r
654 // by the query. However, this returns some false positives, so may be high.
\r
655 // In addition, we can match multiple fields per doc, so this may be low. Fun, eh?
\r
656 int nDocsReturnedInQuery = (int)docList.totalSize();
\r
657 int nDocsProcessed = 0;
\r
658 int firstItemInPage = pageNum*pageSize;
\r
659 while (iter.hasNext()) {
\r
660 DocumentModel docModel = iter.next();
\r
661 AuthorityRefDocList.AuthorityRefDocItem ilistItem;
\r
663 String docType = docModel.getDocumentType().getName(); // REM - This will be a tentant qualified document type
\r
664 docType = ServiceBindingUtils.getUnqualifiedTenantDocType(docType);
\r
665 ServiceBindingType sb = queriedServiceBindings.get(docType);
\r
667 throw new RuntimeException(
\r
668 "getAuthorityRefDocs: No Service Binding for docType: " + docType);
\r
671 if (list == null) { // no list - should be update refName case.
\r
672 if (newAuthorityRefName == null) {
\r
673 throw new InternalError("processRefObjsDocList() called with neither an itemList nor a new RefName!");
\r
677 firstItemInPage = 0; // Do not paginate if updating, rather than building list
\r
678 } else { // Have a list - refObjs case
\r
679 if (newAuthorityRefName != null) {
\r
680 throw new InternalError("processRefObjsDocList() called with both an itemList and a new RefName!");
\r
682 if(firstItemInPage > 100) {
\r
683 logger.warn("Processing a large offset (size:{}, num:{}) for refObjs - will be expensive!!!",
\r
684 pageSize, pageNum);
\r
686 // Note that we have to go through check all the fields to determine the actual page start
\r
687 ilistItem = new AuthorityRefDocList.AuthorityRefDocItem();
\r
688 String csid = NuxeoUtils.getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
\r
690 String itemRefName = getRefname(docModel);
\r
691 ilistItem.setRefName(itemRefName);
\r
692 } catch (ClientException ce) {
\r
693 throw new RuntimeException(
\r
694 "processRefObjsDocList: Problem fetching refName from item Object: "
\r
695 + ce.getLocalizedMessage());
\r
697 ilistItem.setDocId(csid);
\r
699 UriTemplateRegistryKey key = new UriTemplateRegistryKey(tenantId, docType);
\r
700 StoredValuesUriTemplate template = registry.get(key);
\r
701 if (template != null) {
\r
702 Map<String, String> additionalValues = new HashMap<String, String>();
\r
703 if (template.getUriTemplateType() == UriTemplateFactory.RESOURCE) {
\r
704 additionalValues.put(UriTemplateFactory.IDENTIFIER_VAR, csid);
\r
705 uri = template.buildUri(additionalValues);
\r
706 } else if (template.getUriTemplateType() == UriTemplateFactory.ITEM) {
\r
708 String inAuthorityCsid = (String) docModel.getPropertyValue("inAuthority"); // AuthorityItemJAXBSchema.IN_AUTHORITY
\r
709 additionalValues.put(UriTemplateFactory.IDENTIFIER_VAR, inAuthorityCsid);
\r
710 additionalValues.put(UriTemplateFactory.ITEM_IDENTIFIER_VAR, csid);
\r
711 uri = template.buildUri(additionalValues);
\r
712 } catch (Exception e) {
\r
713 logger.warn("Could not extract inAuthority property from authority item record: " + e.getMessage());
\r
715 } else if (template.getUriTemplateType() == UriTemplateFactory.CONTACT) {
\r
716 // FIXME: Generating contact sub-resource URIs requires additional work,
\r
717 // as a follow-on to CSPACE-5271 - ADR 2012-08-16
\r
718 // Sets the default (empty string) value for uri, for now
\r
720 logger.warn("Unrecognized URI template type = " + template.getUriTemplateType());
\r
721 // Sets the default (empty string) value for uri
\r
723 } else { // (if template == null)
\r
724 logger.warn("Could not retrieve URI template from registry via tenant ID "
\r
725 + tenantId + " and docType " + docType);
\r
726 // Sets the default (empty string) value for uri
\r
728 ilistItem.setUri(uri);
\r
730 ilistItem.setWorkflowState(docModel.getCurrentLifeCycleState());
\r
731 ilistItem.setUpdatedAt(DocHandlerBase.getUpdatedAtAsString(docModel));
\r
732 } catch (Exception e) {
\r
733 logger.error("Error getting core values for doc [" + csid + "]: " + e.getLocalizedMessage());
\r
735 ilistItem.setDocType(docType);
\r
736 ilistItem.setDocNumber(
\r
737 ServiceBindingUtils.getMappedFieldInDoc(sb, ServiceBindingUtils.OBJ_NUMBER_PROP, docModel));
\r
738 ilistItem.setDocName(
\r
739 ServiceBindingUtils.getMappedFieldInDoc(sb, ServiceBindingUtils.OBJ_NAME_PROP, docModel));
\r
741 // Now, we have to loop over the authRefFieldsByService to figure
\r
742 // out which field(s) matched this.
\r
743 List<AuthRefConfigInfo> matchingAuthRefFields = authRefFieldsByService.get(docType);
\r
744 if (matchingAuthRefFields == null || matchingAuthRefFields.isEmpty()) {
\r
745 throw new RuntimeException(
\r
746 "getAuthorityRefDocs: internal logic error: can't fetch authRefFields for DocType.");
\r
748 //String authRefAncestorField = "";
\r
749 //String authRefDescendantField = "";
\r
750 //String sourceField = "";
\r
752 ArrayList<RefNameServiceUtils.AuthRefInfo> foundProps = new ArrayList<RefNameServiceUtils.AuthRefInfo>();
\r
754 findAuthRefPropertiesInDoc(docModel, matchingAuthRefFields, refName, matchBaseOnly, foundProps); // REM - side effect that foundProps is set
\r
755 if(!foundProps.isEmpty()) {
\r
756 int nRefsFoundInDoc = 0;
\r
757 for (RefNameServiceUtils.AuthRefInfo ari : foundProps) {
\r
758 if (ilistItem != null) {
\r
759 // So this is a true positive, and not a false one. We have to consider pagination now.
\r
760 if(nRefsFoundTotal >= firstItemInPage) { // skipped enough already
\r
761 if (nRefsFoundInDoc == 0) { // First one?
\r
762 ilistItem.setSourceField(ari.getQualifiedDisplayName());
\r
763 } else { // duplicates from one object
\r
764 ilistItem = cloneAuthRefDocItem(ilistItem, ari.getQualifiedDisplayName());
\r
766 list.add(ilistItem);
\r
767 nRefsFoundInDoc++; // Only increment if processed, or clone logic above will fail
\r
769 } else { // update refName case
\r
770 Property propToUpdate = ari.getProperty();
\r
771 propToUpdate.setValue(newAuthorityRefName);
\r
773 nRefsFoundTotal++; // Whether we processed or not, we found - essential to pagination logic
\r
775 } else if(ilistItem != null) {
\r
776 String docRefName = ilistItem.getRefName();
\r
778 (docRefName!=null && docRefName.startsWith(refName))
\r
779 :refName.equals(docRefName)) {
\r
780 // We found the self for an item
\r
782 logger.debug("getAuthorityRefDocs: Result: "
\r
783 + docType + " [" + NuxeoUtils.getCsid(docModel)
\r
784 + "] appears to be self for: ["
\r
787 logger.debug("getAuthorityRefDocs: Result: "
\r
788 + docType + " [" + NuxeoUtils.getCsid(docModel)
\r
789 + "] does not reference ["
\r
793 } catch (ClientException ce) {
\r
794 throw new RuntimeException(
\r
795 "getAuthorityRefDocs: Problem fetching values from repo: " + ce.getLocalizedMessage());
\r
798 // Done processing that doc. Are we done with the whole page?
\r
799 // Note pageSize <=0 means do them all
\r
800 if((pageSize > 0) && ((nRefsFoundTotal-firstItemInPage)>=pageSize)) {
\r
801 // Quitting early, so we need to estimate the total. Assume one per doc
\r
802 // for the rest of the docs we matched in the query
\r
803 int unprocessedDocs = nDocsReturnedInQuery - nDocsProcessed;
\r
804 if(unprocessedDocs>0) {
\r
805 // We generally match ourselves in the keyword search. If we already saw ourselves
\r
806 // then do not try to correct for this. Otherwise, decrement the total.
\r
807 // Yes, this is fairly goofy, but the whole estimation mechanism is goofy.
\r
810 nRefsFoundTotal += unprocessedDocs;
\r
814 } // close while(iterator)
\r
815 return nRefsFoundTotal;
\r
818 private static AuthorityRefDocList.AuthorityRefDocItem cloneAuthRefDocItem(
\r
819 AuthorityRefDocList.AuthorityRefDocItem ilistItem, String sourceField) {
\r
820 AuthorityRefDocList.AuthorityRefDocItem newlistItem = new AuthorityRefDocList.AuthorityRefDocItem();
\r
821 newlistItem.setDocId(ilistItem.getDocId());
\r
822 newlistItem.setDocName(ilistItem.getDocName());
\r
823 newlistItem.setDocNumber(ilistItem.getDocNumber());
\r
824 newlistItem.setDocType(ilistItem.getDocType());
\r
825 newlistItem.setUri(ilistItem.getUri());
\r
826 newlistItem.setSourceField(sourceField);
\r
827 return newlistItem;
\r
830 public static List<AuthRefInfo> findAuthRefPropertiesInDoc(
\r
831 DocumentModel docModel,
\r
832 List<AuthRefConfigInfo> authRefFieldInfo,
\r
833 String refNameToMatch,
\r
834 List<AuthRefInfo> foundProps) {
\r
835 return findAuthRefPropertiesInDoc(docModel, authRefFieldInfo,
\r
836 refNameToMatch, false, foundProps);
\r
839 public static List<AuthRefInfo> findAuthRefPropertiesInDoc(
\r
840 DocumentModel docModel,
\r
841 List<AuthRefConfigInfo> authRefFieldInfo,
\r
842 String refNameToMatch,
\r
843 boolean matchBaseOnly,
\r
844 List<AuthRefInfo> foundProps) {
\r
845 // Assume that authRefFieldInfo is keyed by the field name (possibly mapped for UI)
\r
846 // and the values are elPaths to the field, where intervening group structures in
\r
847 // lists of complex structures are replaced with "*". Thus, valid paths include
\r
848 // the following (note that the ServiceBindingUtils prepend schema names to configured values):
\r
849 // "schemaname:fieldname"
\r
850 // "schemaname:scalarlistname"
\r
851 // "schemaname:complexfieldname/fieldname"
\r
852 // "schemaname:complexlistname/*/fieldname"
\r
853 // "schemaname:complexlistname/*/scalarlistname"
\r
854 // "schemaname:complexlistname/*/complexfieldname/fieldname"
\r
855 // "schemaname:complexlistname/*/complexlistname/*/fieldname"
\r
857 for (AuthRefConfigInfo arci : authRefFieldInfo) {
\r
859 // Get first property and work down as needed.
\r
860 Property prop = docModel.getProperty(arci.pathEls[0]);
\r
861 findAuthRefPropertiesInProperty(foundProps, prop, arci, 0, refNameToMatch, matchBaseOnly);
\r
862 } catch (Exception e) {
\r
863 logger.error("Problem fetching property: " + arci.pathEls[0]);
\r
869 private static List<AuthRefInfo> findAuthRefPropertiesInProperty(
\r
870 List<AuthRefInfo> foundProps,
\r
872 AuthRefConfigInfo arci,
\r
873 int pathStartIndex, // Supports recursion and we work down the path
\r
874 String refNameToMatch,
\r
875 boolean matchBaseOnly ) {
\r
876 if (pathStartIndex >= arci.pathEls.length) {
\r
877 throw new ArrayIndexOutOfBoundsException("Index = " + pathStartIndex + " for path: "
\r
878 + arci.pathEls.toString());
\r
880 AuthRefInfo ari = null;
\r
881 if (prop == null) {
\r
885 if (prop instanceof StringProperty) { // scalar string
\r
886 addARIifMatches(refNameToMatch, matchBaseOnly, arci, prop, foundProps);
\r
887 } else if (prop instanceof List) {
\r
888 List<Property> propList = (List<Property>) prop;
\r
889 // run through list. Must either be list of Strings, or Complex
\r
890 for (Property listItemProp : propList) {
\r
891 if (listItemProp instanceof StringProperty) {
\r
892 if (arci.pathEls.length - pathStartIndex != 1) {
\r
893 logger.error("Configuration for authRefs does not match schema structure: "
\r
894 + arci.pathEls.toString());
\r
897 addARIifMatches(refNameToMatch, matchBaseOnly, arci, listItemProp, foundProps);
\r
899 } else if (listItemProp.isComplex()) {
\r
900 // Just recurse to handle this. Note that since this is a list of complex,
\r
901 // which should look like listName/*/... we add 2 to the path start index
\r
902 findAuthRefPropertiesInProperty(foundProps, listItemProp, arci,
\r
903 pathStartIndex + 2, refNameToMatch, matchBaseOnly);
\r
905 logger.error("Configuration for authRefs does not match schema structure: "
\r
906 + arci.pathEls.toString());
\r
910 } else if (prop.isComplex()) {
\r
911 String localPropName = arci.pathEls[pathStartIndex];
\r
913 Property localProp = prop.get(localPropName);
\r
914 // Now just recurse, pushing down the path 1 step
\r
915 findAuthRefPropertiesInProperty(foundProps, localProp, arci,
\r
916 pathStartIndex, refNameToMatch, matchBaseOnly);
\r
917 } catch (PropertyNotFoundException pnfe) {
\r
918 logger.error("Could not find property: [" + localPropName + "] in path: "
\r
919 + arci.getFullPath());
\r
920 // Fall through - ari will be null and we will continue...
\r
923 logger.error("Configuration for authRefs does not match schema structure: "
\r
924 + arci.pathEls.toString());
\r
928 foundProps.add(ari); //FIXME: REM - This is dead code. 'ari' is never touched after being initalized to null. Why?
\r
934 private static void addARIifMatches(
\r
935 String refNameToMatch,
\r
936 boolean matchBaseOnly,
\r
937 AuthRefConfigInfo arci,
\r
939 List<AuthRefInfo> foundProps) {
\r
940 // Need to either match a passed refName
\r
941 // OR have no refName to match but be non-empty
\r
943 String value = (String) prop.getValue();
\r
944 if (((refNameToMatch != null) &&
\r
946 (value!=null && value.startsWith(refNameToMatch))
\r
947 :refNameToMatch.equals(value)))
\r
948 || ((refNameToMatch == null) && Tools.notBlank(value))) {
\r
950 logger.debug("Found a match on property: " + prop.getPath() + " with value: [" + value + "]");
\r
951 AuthRefInfo ari = new AuthRefInfo(arci, prop);
\r
952 foundProps.add(ari);
\r
954 } catch (PropertyException pe) {
\r
955 logger.debug("PropertyException on: " + prop.getPath() + pe.getLocalizedMessage());
\r
960 * Identifies whether the refName was found in the supplied field. If passed
\r
961 * a new RefName, will set that into fields in which the old one was found.
\r
963 * Only works for: * Scalar fields * Repeatable scalar fields (aka
\r
964 * multi-valued fields)
\r
966 * Does not work for: * Structured fields (complexTypes) * Repeatable
\r
967 * structured fields (repeatable complexTypes) private static int
\r
968 * refNameFoundInField(String oldRefName, Property fieldValue, String
\r
969 * newRefName) { int nFound = 0; if (fieldValue instanceof List) {
\r
970 * List<Property> fieldValueList = (List) fieldValue; for (Property
\r
971 * listItemValue : fieldValueList) { try { if ((listItemValue instanceof
\r
972 * StringProperty) &&
\r
973 * oldRefName.equalsIgnoreCase((String)listItemValue.getValue())) {
\r
974 * nFound++; if(newRefName!=null) { fieldValue.setValue(newRefName); } else
\r
975 * { // We cannot quit after the first, if we are replacing values. // If we
\r
976 * are just looking (not replacing), finding one is enough. break; } } }
\r
977 * catch( PropertyException pe ) {} } } else { try { if ((fieldValue
\r
978 * instanceof StringProperty) &&
\r
979 * oldRefName.equalsIgnoreCase((String)fieldValue.getValue())) { nFound++;
\r
980 * if(newRefName!=null) { fieldValue.setValue(newRefName); } } } catch(
\r
981 * PropertyException pe ) {} } return nFound; }
\r