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