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