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