]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
e44ad6ee03dfd861a34bb19061d3aed5bb9a1458
[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 package org.collectionspace.services.nuxeo.client.java;
18
19 import java.io.Serializable;
20 import java.sql.SQLException;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Comparator;
24 import java.util.HashSet;
25 import java.util.Hashtable;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.UUID;
31
32 import javax.sql.rowset.CachedRowSet;
33 import javax.ws.rs.WebApplicationException;
34 import javax.ws.rs.core.MultivaluedMap;
35
36 import org.collectionspace.services.client.CollectionSpaceClient;
37 import org.collectionspace.services.client.IQueryManager;
38 import org.collectionspace.services.client.PoxPayloadIn;
39 import org.collectionspace.services.client.PoxPayloadOut;
40 import org.collectionspace.services.client.Profiler;
41 import org.collectionspace.services.client.workflow.WorkflowClient;
42 import org.collectionspace.services.common.context.ServiceContext;
43 import org.collectionspace.services.common.query.QueryContext;
44 import org.collectionspace.services.common.repository.RepositoryClient;
45 import org.collectionspace.services.common.storage.JDBCTools;
46 import org.collectionspace.services.common.storage.PreparedStatementSimpleBuilder;
47 import org.collectionspace.services.lifecycle.TransitionDef;
48 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
49 import org.collectionspace.services.common.document.BadRequestException;
50 import org.collectionspace.services.common.document.DocumentException;
51 import org.collectionspace.services.common.document.DocumentFilter;
52 import org.collectionspace.services.common.document.DocumentHandler;
53 import org.collectionspace.services.common.document.DocumentNotFoundException;
54 import org.collectionspace.services.common.document.DocumentHandler.Action;
55 import org.collectionspace.services.common.document.DocumentWrapper;
56 import org.collectionspace.services.common.document.DocumentWrapperImpl;
57 import org.collectionspace.services.common.document.TransactionException;
58 import org.collectionspace.services.config.tenant.RepositoryDomainType;
59 import org.nuxeo.common.utils.IdUtils;
60 import org.nuxeo.ecm.core.api.ClientException;
61 import org.nuxeo.ecm.core.api.DocumentModel;
62 import org.nuxeo.ecm.core.api.DocumentModelList;
63 import org.nuxeo.ecm.core.api.IterableQueryResult;
64 import org.nuxeo.ecm.core.api.VersioningOption;
65 import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
66 import org.nuxeo.ecm.core.api.DocumentRef;
67 import org.nuxeo.ecm.core.api.IdRef;
68 import org.nuxeo.ecm.core.api.PathRef;
69 import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
70 import org.nuxeo.runtime.transaction.TransactionRuntimeException;
71
72 //
73 // CSPACE-5036 - How to make CMISQL queries from Nuxeo
74 //
75 import org.apache.chemistry.opencmis.commons.server.CallContext;
76 import org.apache.chemistry.opencmis.server.impl.CallContextImpl;
77 import org.collectionspace.services.common.CSWebApplicationException;
78 import org.collectionspace.services.common.ServiceMain;
79 import org.collectionspace.services.common.api.Tools;
80 import org.collectionspace.services.common.config.ConfigUtils;
81 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
82 import org.collectionspace.services.common.config.TenantBindingUtils;
83 import org.collectionspace.services.common.storage.PreparedStatementBuilder;
84 import org.collectionspace.services.config.tenant.TenantBindingType;
85 import org.nuxeo.ecm.core.opencmis.impl.server.NuxeoCmisService;
86 import org.nuxeo.ecm.core.opencmis.impl.server.NuxeoRepository;
87 import org.slf4j.Logger;
88 import org.slf4j.LoggerFactory;
89
90 /**
91  * RepositoryJavaClient is used to perform CRUD operations on documents in Nuxeo
92  * repository using Remote Java APIs. It uses
93  *
94  * @see DocumentHandler as IOHandler with the client.
95  *
96  * $LastChangedRevision: $ $LastChangedDate: $
97  */
98 public class RepositoryJavaClientImpl implements RepositoryClient<PoxPayloadIn, PoxPayloadOut> {
99
100     /**
101      * The logger.
102      */
103     private final Logger logger = LoggerFactory.getLogger(RepositoryJavaClientImpl.class);
104 //    private final Logger profilerLogger = LoggerFactory.getLogger("remperf");
105 //    private String foo = Profiler.createLogger();
106     public static final String NUXEO_CORE_TYPE_DOMAIN = "Domain";
107     public static final String NUXEO_CORE_TYPE_WORKSPACEROOT = "WorkspaceRoot";
108     // FIXME: Get this value from an existing constant, if available
109     private static final String USER_SUPPLIED_WILDCARD = "*";
110     private static final String USER_SUPPLIED_WILDCARD_REGEX = "\\" + USER_SUPPLIED_WILDCARD;
111     private static final String USER_SUPPLIED_ANCHOR_CHAR = "^";
112
113     
114     /**
115      * Instantiates a new repository java client impl.
116      */
117     public RepositoryJavaClientImpl() {
118         //Empty constructor
119     }
120
121     public void assertWorkflowState(ServiceContext ctx,
122             DocumentModel docModel) throws DocumentNotFoundException, ClientException {
123         MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
124         if (queryParams != null) {
125             //
126             // Look for the workflow "delete" query param and see if we need to assert that the
127             // docModel is in a non-deleted workflow state.
128             //
129             String currentState = docModel.getCurrentLifeCycleState();
130             String includeDeletedStr = queryParams.getFirst(WorkflowClient.WORKFLOW_QUERY_NONDELETED);
131             boolean includeDeleted = includeDeletedStr == null ? true : Boolean.parseBoolean(includeDeletedStr);
132             if (includeDeleted == false) {
133                 //
134                 // We don't wanted soft-deleted object, so throw an exception if this one is soft-deleted.
135                 //
136                 if (currentState.equalsIgnoreCase(WorkflowClient.WORKFLOWSTATE_DELETED)) {
137                     String msg = "The GET assertion that docModel not be in 'deleted' workflow state failed.";
138                     logger.debug(msg);
139                     throw new DocumentNotFoundException(msg);
140                 }
141             }
142         }
143     }
144
145     /**
146      * create document in the Nuxeo repository
147      *
148      * @param ctx service context under which this method is invoked
149      * @param handler should be used by the caller to provide and transform the
150      * document
151      * @return id in repository of the newly created document
152      * @throws BadRequestException
153      * @throws TransactionException
154      * @throws DocumentException
155      */
156     @Override
157     public String create(ServiceContext ctx,
158             DocumentHandler handler) throws BadRequestException,
159             TransactionException, DocumentException {
160
161         String docType = NuxeoUtils.getTenantQualifiedDocType(ctx); //ctx.getDocumentType();
162         if (docType == null) {
163             throw new IllegalArgumentException(
164                     "RepositoryJavaClient.create: docType is missing");
165         }
166
167         if (handler == null) {
168             throw new IllegalArgumentException(
169                     "RepositoryJavaClient.create: handler is missing");
170         }
171         String nuxeoWspaceId = ctx.getRepositoryWorkspaceId();
172         if (nuxeoWspaceId == null) {
173             throw new DocumentNotFoundException(
174                     "Unable to find workspace for service " + ctx.getServiceName()
175                     + " check if the workspace exists in the Nuxeo repository");
176         }
177
178         RepositoryInstance repoSession = null;
179         try {
180             handler.prepare(Action.CREATE);
181             repoSession = getRepositorySession(ctx);
182             DocumentRef nuxeoWspace = new IdRef(nuxeoWspaceId);
183             DocumentModel wspaceDoc = repoSession.getDocument(nuxeoWspace);
184             String wspacePath = wspaceDoc.getPathAsString();
185             //give our own ID so PathRef could be constructed later on
186             String id = IdUtils.generateId(UUID.randomUUID().toString());
187             // create document model
188             DocumentModel doc = repoSession.createDocumentModel(wspacePath, id, docType);
189             /* Check for a versioned document, and check In and Out before we proceed.
190              * This does not work as we do not have the uid schema on our docs.
191              if(((DocumentModelHandler) handler).supportsVersioning()) {
192              doc.setProperty("uid","major_version",1);
193              doc.setProperty("uid","minor_version",0);
194              }
195              */
196             ((DocumentModelHandler) handler).setRepositorySession(repoSession);
197             DocumentWrapper<DocumentModel> wrapDoc = new DocumentWrapperImpl<DocumentModel>(doc);
198             handler.handle(Action.CREATE, wrapDoc);
199             // create document with documentmodel
200             doc = repoSession.createDocument(doc);
201             repoSession.save();
202 // TODO for sub-docs need to call into the handler to let it deal with subitems. Pass in the id,
203 // and assume the handler has the state it needs (doc fragments). 
204             handler.complete(Action.CREATE, wrapDoc);
205             return id;
206         } catch (BadRequestException bre) {
207             throw bre;
208         } catch (Exception e) {
209             logger.error("Caught exception ", e);
210             throw new DocumentException(e);
211         } finally {
212             if (repoSession != null) {
213                 releaseRepositorySession(ctx, repoSession);
214             }
215         }
216
217     }
218
219     /**
220      * get document from the Nuxeo repository
221      *
222      * @param ctx service context under which this method is invoked
223      * @param id of the document to retrieve
224      * @param handler should be used by the caller to provide and transform the
225      * document
226      * @throws DocumentNotFoundException if the document cannot be found in the
227      * repository
228      * @throws TransactionException
229      * @throws DocumentException
230      */
231     @Override
232     public void get(ServiceContext ctx, String id, DocumentHandler handler)
233             throws DocumentNotFoundException, TransactionException, DocumentException {
234
235         if (handler == null) {
236             throw new IllegalArgumentException(
237                     "RepositoryJavaClient.get: handler is missing");
238         }
239
240         RepositoryInstance repoSession = null;
241         try {
242             handler.prepare(Action.GET);
243             repoSession = getRepositorySession(ctx);
244             DocumentRef docRef = NuxeoUtils.createPathRef(ctx, id);
245             DocumentModel docModel = null;
246             try {
247                 docModel = repoSession.getDocument(docRef);
248                 assertWorkflowState(ctx, docModel);
249             } catch (ClientException ce) {
250                 String msg = logException(ce, "Could not find document with CSID=" + id);
251                 throw new DocumentNotFoundException(msg, ce);
252             }
253             //
254             // Set repository session to handle the document
255             //
256             ((DocumentModelHandler) handler).setRepositorySession(repoSession);
257             DocumentWrapper<DocumentModel> wrapDoc = new DocumentWrapperImpl<DocumentModel>(docModel);
258             handler.handle(Action.GET, wrapDoc);
259             handler.complete(Action.GET, wrapDoc);
260         } catch (IllegalArgumentException iae) {
261             throw iae;
262         } catch (DocumentException de) {
263             throw de;
264         } catch (Exception e) {
265             if (logger.isDebugEnabled()) {
266                 logger.debug("Caught exception ", e);
267             }
268             throw new DocumentException(e);
269         } finally {
270             if (repoSession != null) {
271                 releaseRepositorySession(ctx, repoSession);
272             }
273         }
274     }
275
276     /**
277      * get a document from the Nuxeo repository, using the docFilter params.
278      *
279      * @param ctx service context under which this method is invoked
280      * @param handler should be used by the caller to provide and transform the
281      * document. Handler must have a docFilter set to return a single item.
282      * @throws DocumentNotFoundException if the document cannot be found in the
283      * repository
284      * @throws TransactionException
285      * @throws DocumentException
286      */
287     @Override
288     public void get(ServiceContext ctx, DocumentHandler handler)
289             throws DocumentNotFoundException, TransactionException, DocumentException {
290         QueryContext queryContext = new QueryContext(ctx, handler);
291         RepositoryInstance repoSession = null;
292
293         try {
294             handler.prepare(Action.GET);
295             repoSession = getRepositorySession(ctx);
296
297             DocumentModelList docList = null;
298             // force limit to 1, and ignore totalSize
299             String query = NuxeoUtils.buildNXQLQuery(ctx, queryContext);
300             docList = repoSession.query(query, null, 1, 0, false);
301             if (docList.size() != 1) {
302                 throw new DocumentNotFoundException("No document found matching filter params: " + query);
303             }
304             DocumentModel doc = docList.get(0);
305
306             if (logger.isDebugEnabled()) {
307                 logger.debug("Executed NXQL query: " + query);
308             }
309
310             //set reposession to handle the document
311             ((DocumentModelHandler) handler).setRepositorySession(repoSession);
312             DocumentWrapper<DocumentModel> wrapDoc = new DocumentWrapperImpl<DocumentModel>(doc);
313             handler.handle(Action.GET, wrapDoc);
314             handler.complete(Action.GET, wrapDoc);
315         } catch (IllegalArgumentException iae) {
316             throw iae;
317         } catch (DocumentException de) {
318             throw de;
319         } catch (Exception e) {
320             if (logger.isDebugEnabled()) {
321                 logger.debug("Caught exception ", e);
322             }
323             throw new DocumentException(e);
324         } finally {
325             if (repoSession != null) {
326                 releaseRepositorySession(ctx, repoSession);
327             }
328         }
329     }
330
331     public DocumentWrapper<DocumentModel> getDoc(
332             RepositoryInstance repoSession,
333             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
334             String csid) throws DocumentNotFoundException, DocumentException {
335         DocumentWrapper<DocumentModel> wrapDoc = null;
336
337         try {
338             DocumentRef docRef = NuxeoUtils.createPathRef(ctx, csid);
339             DocumentModel doc = null;
340             try {
341                 doc = repoSession.getDocument(docRef);
342             } catch (ClientException ce) {
343                 String msg = logException(ce, "Could not find document with CSID=" + csid);
344                 throw new DocumentNotFoundException(msg, ce);
345             }
346             wrapDoc = new DocumentWrapperImpl<DocumentModel>(doc);
347         } catch (IllegalArgumentException iae) {
348             throw iae;
349         } catch (DocumentException de) {
350             throw de;
351         }
352
353         return wrapDoc;
354     }
355
356     /**
357      * Get wrapped documentModel from the Nuxeo repository. The search is
358      * restricted to the workspace of the current context.
359      *
360      * @param ctx service context under which this method is invoked
361      * @param csid of the document to retrieve
362      * @throws DocumentNotFoundException
363      * @throws TransactionException
364      * @throws DocumentException
365      * @return a wrapped documentModel
366      */
367     @Override
368     public DocumentWrapper<DocumentModel> getDoc(
369             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
370             String csid) throws DocumentNotFoundException, TransactionException, DocumentException {
371         RepositoryInstance repoSession = null;
372         DocumentWrapper<DocumentModel> wrapDoc = null;
373
374         try {
375             // Open a new repository session
376             repoSession = getRepositorySession(ctx);
377             wrapDoc = getDoc(repoSession, ctx, csid);
378         } catch (IllegalArgumentException iae) {
379             throw iae;
380         } catch (DocumentException de) {
381             throw de;
382         } catch (Exception e) {
383             if (logger.isDebugEnabled()) {
384                 logger.debug("Caught exception ", e);
385             }
386             throw new DocumentException(e);
387         } finally {
388             if (repoSession != null) {
389                 releaseRepositorySession(ctx, repoSession);
390             }
391         }
392
393         if (logger.isWarnEnabled() == true) {
394             logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
395         }
396         return wrapDoc;
397     }
398
399     public DocumentWrapper<DocumentModel> findDoc(
400             RepositoryInstance repoSession,
401             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
402             String whereClause)
403             throws DocumentNotFoundException, DocumentException {
404         DocumentWrapper<DocumentModel> wrapDoc = null;
405
406         try {
407             QueryContext queryContext = new QueryContext(ctx, whereClause);
408             DocumentModelList docList = null;
409             // force limit to 1, and ignore totalSize
410             String query = NuxeoUtils.buildNXQLQuery(ctx, queryContext);
411             docList = repoSession.query(query,
412                     null, //Filter
413                     1, //limit
414                     0, //offset
415                     false); //countTotal
416             if (docList.size() != 1) {
417                 if (logger.isDebugEnabled()) {
418                     logger.debug("findDoc: Query found: " + docList.size() + " items.");
419                     logger.debug(" Query: " + query);
420                 }
421                 throw new DocumentNotFoundException("No document found matching filter params: " + query);
422             }
423             DocumentModel doc = docList.get(0);
424             wrapDoc = new DocumentWrapperImpl<DocumentModel>(doc);
425         } catch (IllegalArgumentException iae) {
426             throw iae;
427         } catch (DocumentException de) {
428             throw de;
429         } catch (Exception e) {
430             if (logger.isDebugEnabled()) {
431                 logger.debug("Caught exception ", e);
432             }
433             throw new DocumentException(e);
434         }
435
436         return wrapDoc;
437     }
438
439     /**
440      * find wrapped documentModel from the Nuxeo repository
441      *
442      * @param ctx service context under which this method is invoked
443      * @param whereClause where NXQL where clause to get the document
444      * @throws DocumentNotFoundException
445      * @throws TransactionException
446      * @throws DocumentException
447      * @return a wrapped documentModel retrieved by the repository query
448      */
449     @Override
450     public DocumentWrapper<DocumentModel> findDoc(
451             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
452             String whereClause)
453             throws DocumentNotFoundException, TransactionException, DocumentException {
454         RepositoryInstance repoSession = null;
455         DocumentWrapper<DocumentModel> wrapDoc = null;
456
457         try {
458             repoSession = getRepositorySession(ctx);
459             wrapDoc = findDoc(repoSession, ctx, whereClause);
460         } catch (Exception e) {
461             throw new DocumentException("Unable to create a Nuxeo repository session.", e);
462         } finally {
463             if (repoSession != null) {
464                 releaseRepositorySession(ctx, repoSession);
465             }
466         }
467
468         if (logger.isWarnEnabled() == true) {
469             logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
470         }
471
472         return wrapDoc;
473     }
474
475     /**
476      * find doc and return CSID from the Nuxeo repository
477      *
478      * @param repoSession
479      * @param ctx service context under which this method is invoked
480      * @param whereClause where NXQL where clause to get the document
481      * @throws DocumentNotFoundException
482      * @throws TransactionException
483      * @throws DocumentException
484      * @return the CollectionSpace ID (CSID) of the requested document
485      */
486     @Override
487     public String findDocCSID(RepositoryInstance repoSession,
488             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx, String whereClause)
489             throws DocumentNotFoundException, TransactionException, DocumentException {
490         String csid = null;
491         boolean releaseSession = false;
492         try {
493             if (repoSession == null) {
494                 repoSession = this.getRepositorySession(ctx);
495                 releaseSession = true;
496             }
497             DocumentWrapper<DocumentModel> wrapDoc = findDoc(repoSession, ctx, whereClause);
498             DocumentModel docModel = wrapDoc.getWrappedObject();
499             csid = NuxeoUtils.getCsid(docModel);//NuxeoUtils.extractId(docModel.getPathAsString());
500         } catch (DocumentNotFoundException dnfe) {
501             throw dnfe;
502         } catch (IllegalArgumentException iae) {
503             throw iae;
504         } catch (DocumentException de) {
505             throw de;
506         } catch (Exception e) {
507             if (logger.isDebugEnabled()) {
508                 logger.debug("Caught exception ", e);
509             }
510             throw new DocumentException(e);
511         } finally {
512             if (releaseSession && (repoSession != null)) {
513                 this.releaseRepositorySession(ctx, repoSession);
514             }
515         }
516         return csid;
517     }
518
519     public DocumentWrapper<DocumentModelList> findDocs(
520             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
521             RepositoryInstance repoSession,
522             List<String> docTypes,
523             String whereClause,
524             String orderByClause,
525             int pageSize,
526             int pageNum,
527             boolean computeTotal)
528             throws DocumentNotFoundException, DocumentException {
529         DocumentWrapper<DocumentModelList> wrapDoc = null;
530
531         try {
532             if (docTypes == null || docTypes.size() < 1) {
533                 throw new DocumentNotFoundException(
534                         "The findDocs() method must specify at least one DocumentType.");
535             }
536             DocumentModelList docList = null;
537             QueryContext queryContext = new QueryContext(ctx, whereClause, orderByClause);
538             String query = NuxeoUtils.buildNXQLQuery(docTypes, queryContext);
539             if (logger.isDebugEnabled()) {
540                 logger.debug("findDocs() NXQL: " + query);
541             }
542             docList = repoSession.query(query, null, pageSize, pageSize * pageNum, computeTotal);
543             wrapDoc = new DocumentWrapperImpl<DocumentModelList>(docList);
544         } catch (IllegalArgumentException iae) {
545             throw iae;
546         } catch (Exception e) {
547             if (logger.isDebugEnabled()) {
548                 logger.debug("Caught exception ", e);
549             }
550             throw new DocumentException(e);
551         }
552
553         return wrapDoc;
554     }
555
556     protected static String buildInListForDocTypes(List<String> docTypes) {
557         StringBuilder sb = new StringBuilder();
558         sb.append("(");
559         boolean first = true;
560         for (String docType : docTypes) {
561             if (first) {
562                 first = false;
563             } else {
564                 sb.append(",");
565             }
566             sb.append("'");
567             sb.append(docType);
568             sb.append("'");
569         }
570         sb.append(")");
571         return sb.toString();
572     }
573
574     public DocumentWrapper<DocumentModelList> findDocs(
575             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
576             DocumentHandler handler,
577             RepositoryInstance repoSession,
578             List<String> docTypes)
579             throws DocumentNotFoundException, DocumentException {
580         DocumentWrapper<DocumentModelList> wrapDoc = null;
581
582         DocumentFilter filter = handler.getDocumentFilter();
583         String oldOrderBy = filter.getOrderByClause();
584         if (isClauseEmpty(oldOrderBy) == true) {
585             filter.setOrderByClause(DocumentFilter.ORDER_BY_LAST_UPDATED);
586         }
587         QueryContext queryContext = new QueryContext(ctx, handler);
588
589         try {
590             if (docTypes == null || docTypes.size() < 1) {
591                 throw new DocumentNotFoundException(
592                         "The findDocs() method must specify at least one DocumentType.");
593             }
594             DocumentModelList docList = null;
595             if (handler.isCMISQuery() == true) {
596                 String inList = buildInListForDocTypes(docTypes);
597                 ctx.getQueryParams().add(IQueryManager.SEARCH_RELATED_MATCH_OBJ_DOCTYPES, inList);
598                 docList = getFilteredCMIS(repoSession, ctx, handler, queryContext);
599             } else {
600                 String query = NuxeoUtils.buildNXQLQuery(docTypes, queryContext);
601                 if (logger.isDebugEnabled()) {
602                     logger.debug("findDocs() NXQL: " + query);
603                 }
604                 docList = repoSession.query(query, null, filter.getPageSize(), filter.getOffset(), true);
605             }
606             wrapDoc = new DocumentWrapperImpl<DocumentModelList>(docList);
607         } catch (IllegalArgumentException iae) {
608             throw iae;
609         } catch (Exception e) {
610             if (logger.isDebugEnabled()) {
611                 logger.debug("Caught exception ", e);
612             }
613             throw new DocumentException(e);
614         }
615
616         return wrapDoc;
617     }
618
619     /**
620      * Find a list of documentModels from the Nuxeo repository
621      *
622      * @param docTypes a list of DocType names to match
623      * @param whereClause where the clause to qualify on
624      * @throws DocumentNotFoundException
625      * @throws TransactionException
626      * @throws DocumentException
627      * @return a list of documentModels
628      */
629     @Override
630     public DocumentWrapper<DocumentModelList> findDocs(
631             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
632             List<String> docTypes,
633             String whereClause,
634             int pageSize, int pageNum, boolean computeTotal)
635             throws DocumentNotFoundException, TransactionException, DocumentException {
636         RepositoryInstance repoSession = null;
637         DocumentWrapper<DocumentModelList> wrapDoc = null;
638
639         try {
640             repoSession = getRepositorySession(ctx);
641             wrapDoc = findDocs(ctx, repoSession, docTypes, whereClause, null,
642                     pageSize, pageNum, computeTotal);
643         } catch (IllegalArgumentException iae) {
644             throw iae;
645         } catch (Exception e) {
646             if (logger.isDebugEnabled()) {
647                 logger.debug("Caught exception ", e);
648             }
649             throw new DocumentException(e);
650         } finally {
651             if (repoSession != null) {
652                 releaseRepositorySession(ctx, repoSession);
653             }
654         }
655
656         if (logger.isWarnEnabled() == true) {
657             logger.warn("Returned DocumentModelList instance was created with a repository session that is now closed.");
658         }
659
660         return wrapDoc;
661     }
662
663     /* (non-Javadoc)
664      * @see org.collectionspace.services.common.storage.StorageClient#get(org.collectionspace.services.common.context.ServiceContext, java.util.List, org.collectionspace.services.common.document.DocumentHandler)
665      */
666     @Override
667     public void get(ServiceContext ctx, List<String> csidList, DocumentHandler handler)
668             throws DocumentNotFoundException, TransactionException, DocumentException {
669         if (handler == null) {
670             throw new IllegalArgumentException(
671                     "RepositoryJavaClient.getAll: handler is missing");
672         }
673
674         RepositoryInstance repoSession = null;
675         try {
676             handler.prepare(Action.GET_ALL);
677             repoSession = getRepositorySession(ctx);
678             DocumentModelList docModelList = new DocumentModelListImpl();
679             //FIXME: Should be using NuxeoUtils.createPathRef for security reasons
680             for (String csid : csidList) {
681                 DocumentRef docRef = NuxeoUtils.createPathRef(ctx, csid);
682                 DocumentModel docModel = repoSession.getDocument(docRef);
683                 docModelList.add(docModel);
684             }
685
686             //set reposession to handle the document
687             ((DocumentModelHandler) handler).setRepositorySession(repoSession);
688             DocumentWrapper<DocumentModelList> wrapDoc = new DocumentWrapperImpl<DocumentModelList>(docModelList);
689             handler.handle(Action.GET_ALL, wrapDoc);
690             handler.complete(Action.GET_ALL, wrapDoc);
691         } catch (DocumentException de) {
692             throw de;
693         } catch (Exception e) {
694             if (logger.isDebugEnabled()) {
695                 logger.debug("Caught exception ", e);
696             }
697             throw new DocumentException(e);
698         } finally {
699             if (repoSession != null) {
700                 releaseRepositorySession(ctx, repoSession);
701             }
702         }
703     }
704
705     /**
706      * getAll get all documents for an entity entity service from the Nuxeo
707      * repository
708      *
709      * @param ctx service context under which this method is invoked
710      * @param handler should be used by the caller to provide and transform the
711      * document
712      * @throws DocumentNotFoundException
713      * @throws TransactionException
714      * @throws DocumentException
715      */
716     @Override
717     public void getAll(ServiceContext ctx, DocumentHandler handler)
718             throws DocumentNotFoundException, TransactionException, DocumentException {
719         if (handler == null) {
720             throw new IllegalArgumentException(
721                     "RepositoryJavaClient.getAll: handler is missing");
722         }
723         String nuxeoWspaceId = ctx.getRepositoryWorkspaceId();
724         if (nuxeoWspaceId == null) {
725             throw new DocumentNotFoundException(
726                     "Unable to find workspace for service "
727                     + ctx.getServiceName()
728                     + " check if the workspace exists in the Nuxeo repository.");
729         }
730
731         RepositoryInstance repoSession = null;
732         try {
733             handler.prepare(Action.GET_ALL);
734             repoSession = getRepositorySession(ctx);
735             DocumentRef wsDocRef = new IdRef(nuxeoWspaceId);
736             DocumentModelList docList = repoSession.getChildren(wsDocRef);
737             //set reposession to handle the document
738             ((DocumentModelHandler) handler).setRepositorySession(repoSession);
739             DocumentWrapper<DocumentModelList> wrapDoc = new DocumentWrapperImpl<DocumentModelList>(docList);
740             handler.handle(Action.GET_ALL, wrapDoc);
741             handler.complete(Action.GET_ALL, wrapDoc);
742         } catch (DocumentException de) {
743             throw de;
744         } catch (Exception e) {
745             if (logger.isDebugEnabled()) {
746                 logger.debug("Caught exception ", e);
747             }
748             throw new DocumentException(e);
749         } finally {
750             if (repoSession != null) {
751                 releaseRepositorySession(ctx, repoSession);
752             }
753         }
754     }
755
756     private boolean isClauseEmpty(String theString) {
757         boolean result = true;
758         if (theString != null && !theString.isEmpty()) {
759             result = false;
760         }
761         return result;
762     }
763
764     public DocumentWrapper<DocumentModel> getDocFromCsid(
765             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
766             RepositoryInstance repoSession,
767             String csid)
768             throws Exception {
769         DocumentWrapper<DocumentModel> result = null;
770
771         result = new DocumentWrapperImpl(NuxeoUtils.getDocFromCsid(ctx, repoSession, csid));
772
773         return result;
774     }
775
776     /*
777      * A method to find a CollectionSpace document (of any type) given just a service context and
778      * its CSID.  A search across *all* service workspaces (within a given tenant context) is performed to find
779      * the document
780      * 
781      * This query searches Nuxeo's Hierarchy table where our CSIDs are stored in the "name" column.
782      */
783     @Override
784     public DocumentWrapper<DocumentModel> getDocFromCsid(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
785             String csid)
786             throws Exception {
787         DocumentWrapper<DocumentModel> result = null;
788         RepositoryInstance repoSession = null;
789         try {
790             repoSession = getRepositorySession(ctx);
791             result = getDocFromCsid(ctx, repoSession, csid);
792         } finally {
793             if (repoSession != null) {
794                 releaseRepositorySession(ctx, repoSession);
795             }
796         }
797
798         if (logger.isWarnEnabled() == true) {
799             logger.warn("Returned DocumentModel instance was created with a repository session that is now closed.");
800         }
801
802         return result;
803     }
804
805     /**
806      * Returns a URI value for a document in the Nuxeo repository
807      *
808      * @param wrappedDoc a wrapped documentModel
809      * @throws ClientException
810      * @return a document URI
811      */
812     @Override
813     public String getDocURI(DocumentWrapper<DocumentModel> wrappedDoc) throws ClientException {
814         DocumentModel docModel = wrappedDoc.getWrappedObject();
815         String uri = (String) docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
816                 CollectionSpaceClient.COLLECTIONSPACE_CORE_URI);
817         return uri;
818     }
819
820     /*
821      * See CSPACE-5036 - How to make CMISQL queries from Nuxeo
822      */
823     private IterableQueryResult makeCMISQLQuery(RepositoryInstance repoSession, String query, QueryContext queryContext) {
824         IterableQueryResult result = null;
825
826         // the NuxeoRepository should be constructed only once, then cached
827         // (its construction is expensive)
828         try {
829             NuxeoRepository repo = new NuxeoRepository(
830                     repoSession.getRepositoryName(), repoSession
831                     .getRootDocument().getId());
832             logger.debug("Repository ID:" + repo.getId() + " Root folder:"
833                     + repo.getRootFolderId());
834
835             CallContextImpl callContext = new CallContextImpl(
836                     CallContext.BINDING_LOCAL, repo.getId(), false);
837             callContext.put(CallContext.USERNAME, repoSession.getPrincipal()
838                     .getName());
839             NuxeoCmisService cmisService = new NuxeoCmisService(repo,
840                     callContext, repoSession);
841
842             result = repoSession.queryAndFetch(query, "CMISQL", cmisService);
843         } catch (ClientException e) {
844             // TODO Auto-generated catch block
845             logger.error("Encounter trouble making the following CMIS query: " + query, e);
846         }
847
848         return result;
849     }
850
851     /**
852      * getFiltered get all documents for an entity service from the Document
853      * repository, given filter parameters specified by the handler.
854      *
855      * @param ctx service context under which this method is invoked
856      * @param handler should be used by the caller to provide and transform the
857      * document
858      * @throws DocumentNotFoundException if workspace not found
859      * @throws TransactionException
860      * @throws DocumentException
861      */
862     @Override
863     public void getFiltered(ServiceContext ctx, DocumentHandler handler)
864             throws DocumentNotFoundException, TransactionException, DocumentException {
865
866         DocumentFilter filter = handler.getDocumentFilter();
867         String oldOrderBy = filter.getOrderByClause();
868         if (isClauseEmpty(oldOrderBy) == true) {
869             filter.setOrderByClause(DocumentFilter.ORDER_BY_LAST_UPDATED);
870         }
871         QueryContext queryContext = new QueryContext(ctx, handler);
872
873         RepositoryInstance repoSession = null;
874         try {
875             handler.prepare(Action.GET_ALL);
876             repoSession = getRepositorySession(ctx); //Keeps a refcount here for the repository session so you need to release this when finished
877
878             DocumentModelList docList = null;
879             String query = NuxeoUtils.buildNXQLQuery(ctx, queryContext);
880
881             if (logger.isDebugEnabled()) {
882                 logger.debug("Executing NXQL query: " + query.toString());
883             }
884
885             // If we have limit and/or offset, then pass true to get totalSize
886             // in returned DocumentModelList.
887             Profiler profiler = new Profiler(this, 2);
888             profiler.log("Executing NXQL query: " + query.toString());
889             profiler.start();
890             if (handler.isJDBCQuery() == true) {
891                 docList = getFilteredJDBC(repoSession, ctx, handler);
892             } else if (handler.isCMISQuery() == true) {
893                 docList = getFilteredCMIS(repoSession, ctx, handler, queryContext); //FIXME: REM - Need to deal with paging info in CMIS query
894             } else if ((queryContext.getDocFilter().getOffset() > 0) || (queryContext.getDocFilter().getPageSize() > 0)) {
895                 docList = repoSession.query(query, null,
896                         queryContext.getDocFilter().getPageSize(), queryContext.getDocFilter().getOffset(), true);
897             } else {
898                 docList = repoSession.query(query);
899             }
900             profiler.stop();
901
902             //set repoSession to handle the document
903             ((DocumentModelHandler) handler).setRepositorySession(repoSession);
904             DocumentWrapper<DocumentModelList> wrapDoc = new DocumentWrapperImpl<DocumentModelList>(docList);
905             handler.handle(Action.GET_ALL, wrapDoc);
906             handler.complete(Action.GET_ALL, wrapDoc);
907         } catch (DocumentException de) {
908             throw de;
909         } catch (Exception e) {
910             if (logger.isDebugEnabled()) {
911                 logger.debug("Caught exception ", e); // REM - 1/17/2014: Check for org.nuxeo.ecm.core.api.ClientException and re-attempt
912             }
913             throw new DocumentException(e);
914         } finally {
915             if (repoSession != null) {
916                 releaseRepositorySession(ctx, repoSession);
917             }
918         }
919     }
920
921     /**
922      * Perform a database query, via JDBC and SQL, to retrieve matching records
923      * based on filter criteria.
924      * 
925      * Although this method currently has a general-purpose name, it is
926      * currently dedicated to a specific task: improving performance for
927      * partial term matching queries on authority items / terms, via
928      * the use of a hand-tuned SQL query, rather than the generated SQL
929      * produced by Nuxeo from an NXQL query.
930      * 
931      * @param repoSession a repository session.
932      * @param ctx the service context.
933      * @param handler a relevant document handler.
934      * @return a list of document models matching the search criteria.
935      * @throws Exception 
936      */
937     private DocumentModelList getFilteredJDBC(RepositoryInstance repoSession, ServiceContext ctx, 
938             DocumentHandler handler) throws Exception {
939         DocumentModelList result = new DocumentModelListImpl();
940
941         // FIXME: Get all of the following values from appropriate external constants.
942         //
943         // At present, the two constants below are duplicated in both RepositoryJavaClientImpl
944         // and in AuthorityItemDocumentModelHandler.
945         final String TERM_GROUP_LIST_NAME = "TERM_GROUP_LIST_NAME";
946         final String TERM_GROUP_TABLE_NAME_PARAM = "TERM_GROUP_TABLE_NAME";
947         final String IN_AUTHORITY_PARAM = "IN_AUTHORITY";
948         // Get this from a constant in AuthorityResource or equivalent
949         final String PARENT_WILDCARD = "_ALL_";
950         
951         // Build two SQL statements, to be executed within a single transaction:
952         // the first statement to control join order, and the second statement
953         // representing the actual 'get filtered' query
954         
955         // Build the join control statement
956         //
957         // Per http://www.postgresql.org/docs/9.2/static/runtime-config-query.html#GUC-JOIN-COLLAPSE-LIMIT
958         // "Setting [this value] to 1 prevents any reordering of explicit JOINs.
959         // Thus, the explicit join order specified in the query will be the
960         // actual order in which the relations are joined."
961         // See CSPACE-5945 for further discussion of why this setting is needed.
962         //
963         // Adding this statement is commented out here for now.  It significantly
964         // improved query performance for authority item / term queries where
965         // large numbers of rows were retrieved, but appears to have resulted
966         // in consistently slower-than-desired query performance where zero or
967         // very few records were retrieved. See notes on CSPACE-5945. - ADR 2013-04-09
968         // String joinControlSql = "SET LOCAL join_collapse_limit TO 1;";
969         
970         // Build the query statement
971         //
972         // Start with the default query
973         String selectStatement =
974                 "SELECT DISTINCT commonschema.id"
975                 + " FROM " + handler.getServiceContext().getCommonPartLabel() + " commonschema";
976         
977         String joinClauses =
978                 " INNER JOIN misc"
979                 + "  ON misc.id = commonschema.id"
980                 + " INNER JOIN hierarchy hierarchy_termgroup"
981                 + "  ON hierarchy_termgroup.parentid = misc.id"
982                 + " INNER JOIN "  + handler.getJDBCQueryParams().get(TERM_GROUP_TABLE_NAME_PARAM) + " termgroup"
983                 + "  ON termgroup.id = hierarchy_termgroup.id ";
984
985         String whereClause;
986         MultivaluedMap<String, String> queryParams = ctx.getQueryParams();
987         // Value for replaceable parameter 1 in the query
988         String partialTerm = queryParams.getFirst(IQueryManager.SEARCH_TYPE_PARTIALTERM);
989         // If the value of the partial term query parameter is blank ('pt='),
990         // return all records, subject to restriction by any limit clause
991         if (Tools.isBlank(partialTerm)) {
992            whereClause = "";
993         } else {
994            // Otherwise, return records that match the supplied partial term
995            whereClause =
996                 " WHERE (termgroup.termdisplayname ILIKE ?)";
997         }
998         
999         // At present, results are ordered in code, below, rather than in SQL,
1000         // and the orderByClause below is thus intentionally blank.
1001         //
1002         // To implement the orderByClause below in SQL; e.g. via
1003         // 'ORDER BY termgroup.termdisplayname', the relevant column
1004         // must be returned by the SELECT statement.
1005         String orderByClause = "";
1006         
1007         String limitClause;
1008         TenantBindingConfigReaderImpl tReader =
1009                 ServiceMain.getInstance().getTenantBindingConfigReader();
1010         TenantBindingType tenantBinding = tReader.getTenantBinding(ctx.getTenantId());
1011         String maxListItemsLimit = TenantBindingUtils.getPropertyValue(tenantBinding,
1012                 IQueryManager.MAX_LIST_ITEMS_RETURNED_LIMIT_ON_JDBC_QUERIES);
1013         limitClause =
1014                 " LIMIT " + getMaxItemsLimitOnJdbcQueries(maxListItemsLimit); // implicit int-to-String conversion
1015         
1016         // After building the individual parts of the query, set the values
1017         // of replaceable parameters that will be inserted into that query
1018         // and optionally add restrictions
1019         
1020         List<String> params = new ArrayList<>();
1021         
1022         if (Tools.notBlank(whereClause)) {
1023                         
1024             // Read tenant bindings configuration to determine whether
1025             // to automatically insert leading, as well as trailing, wildcards
1026             // into the term matching string.
1027             String usesStartingWildcard = TenantBindingUtils.getPropertyValue(tenantBinding,
1028                     IQueryManager.TENANT_USES_STARTING_WILDCARD_FOR_PARTIAL_TERM);
1029             // Handle user-provided leading wildcard characters, in the
1030             // configuration where a leading wildcard is not automatically inserted.
1031             // (The user-provided wildcard must be in the first, or "starting"
1032             // character position in the partial term value.)
1033             if (Tools.notBlank(usesStartingWildcard)) {
1034                 if (usesStartingWildcard.equalsIgnoreCase(Boolean.FALSE.toString())) {
1035                     partialTerm = handleProvidedStartingWildcard(partialTerm);
1036                     // Otherwise, in the configuration where a leading wildcard
1037                     // is usually automatically inserted, handle the cases where
1038                     // a user has entered an anchor character in the first position
1039                     // in the starting term value. In those cases, strip that
1040                     // anchor character and don't add a leading wildcard
1041                 } else {
1042                     if (partialTerm.startsWith(USER_SUPPLIED_ANCHOR_CHAR)) {
1043                         partialTerm = partialTerm.substring(1, partialTerm.length());
1044                         // Otherwise, automatically add a leading wildcard
1045                     } else {
1046                         partialTerm = JDBCTools.SQL_WILDCARD + partialTerm;
1047                     }
1048                 }
1049             }
1050             // Add SQL wildcards in the midst of the partial term match search
1051             // expression, whever user-supplied wildcards appear, except in the
1052             // first or last character positions of the search expression.
1053             partialTerm = subtituteWildcardsInPartialTerm(partialTerm);
1054
1055             // If a designated 'anchor character' is present as the last character
1056             // in the search expression, strip that character and don't add
1057             // a trailing wildcard
1058             int lastCharPos = partialTerm.length() - 1;
1059             if (partialTerm.endsWith(USER_SUPPLIED_ANCHOR_CHAR) && lastCharPos > 0) {
1060                     partialTerm = partialTerm.substring(0, lastCharPos);
1061             } else {
1062                 // Otherwise, automatically add a trailing wildcard
1063                 partialTerm = partialTerm + JDBCTools.SQL_WILDCARD;
1064             }
1065             params.add(partialTerm);
1066         }
1067         
1068         // Optionally add restrictions to the default query, based on variables
1069         // in the current request
1070         
1071         // Restrict the query to filter out deleted records, if requested
1072         String includeDeleted = queryParams.getFirst(WorkflowClient.WORKFLOW_QUERY_NONDELETED);
1073         if (includeDeleted != null && includeDeleted.equalsIgnoreCase(Boolean.FALSE.toString())) {
1074             whereClause = whereClause
1075                 + "  AND (misc.lifecyclestate <> '" + WorkflowClient.WORKFLOWSTATE_DELETED + "')";
1076         }
1077
1078         // If a particular authority is specified, restrict the query further
1079         // to return only records within that authority
1080         String inAuthorityValue = (String) handler.getJDBCQueryParams().get(IN_AUTHORITY_PARAM);
1081         if (Tools.notBlank(inAuthorityValue)) {
1082             // Handle the '_ALL_' case for inAuthority
1083             if (inAuthorityValue.equals(PARENT_WILDCARD)) {
1084                 // Add nothing to the query here if it should match within all authorities
1085             } else {
1086                 whereClause = whereClause
1087                     + "  AND (commonschema.inauthority = ?)";
1088                 params.add(inAuthorityValue); // Value for replaceable parameter 2 in the query
1089             }
1090         }
1091         
1092         // Restrict the query further to return only records pertaining to
1093         // the current tenant, unless:
1094         // * Data for this service, in this tenant, is stored in its own,
1095         //   separate repository, rather than being intermingled with other
1096         //   tenants' data in the default repository; or
1097         // * Restriction by tenant ID in JDBC queries has been disabled,
1098         //   via configuration for this tenant, 
1099         if (restrictJDBCQueryByTenantID(tenantBinding, ctx)) {
1100                 joinClauses = joinClauses
1101                     + " INNER JOIN collectionspace_core core"
1102                     + "  ON core.id = hierarchy_termgroup.parentid";
1103                 whereClause = whereClause
1104                     + "  AND (core.tenantid = ?)";
1105                 params.add(ctx.getTenantId()); // Value for replaceable parameter 3 in the query
1106         }
1107         
1108         // Piece together the SQL query from its parts
1109         String querySql = selectStatement + joinClauses + whereClause + orderByClause + limitClause;
1110         
1111         // Note: PostgreSQL 9.2 introduced a change that may improve performance
1112         // of certain queries using JDBC PreparedStatements.  See comments on
1113         // CSPACE-5943 for details.
1114         //
1115         // See a comment above for the reason that the joinControl SQL statement,
1116         // along with its corresponding prepared statement builder, is commented out for now.
1117         // PreparedStatementBuilder joinControlBuilder = new PreparedStatementBuilder(joinControlSql);
1118         PreparedStatementSimpleBuilder queryBuilder = new PreparedStatementSimpleBuilder(querySql, params);
1119         List<PreparedStatementBuilder> builders = new ArrayList<>();
1120         // builders.add(joinControlBuilder);
1121         builders.add(queryBuilder);
1122         String dataSourceName = JDBCTools.NUXEO_DATASOURCE_NAME;
1123         String repositoryName = ctx.getRepositoryName();
1124         final Boolean EXECUTE_WITHIN_TRANSACTION = true;
1125         Set<String> docIds = new HashSet<>();
1126         try {
1127             List<CachedRowSet> resultsList = JDBCTools.executePreparedQueries(builders,
1128                 dataSourceName, repositoryName, EXECUTE_WITHIN_TRANSACTION);
1129
1130             // At least one set of results is expected, from the second prepared
1131             // statement to be executed.
1132             // If fewer results are returned, return an empty list of document models
1133             if (resultsList == null || resultsList.size() < 1) {
1134                 return result; // return an empty list of document models
1135             }
1136             // The join control query (if enabled - it is currently commented
1137             // out as per comments above) will not return results, so query results
1138             // will be the first set of results (rowSet) returned in the list
1139             CachedRowSet queryResults = resultsList.get(0);
1140             
1141             // If the result from executing the query is null or contains zero rows,
1142             // return an empty list of document models
1143             if (queryResults == null) {
1144                 return result; // return an empty list of document models
1145             }
1146             queryResults.last();
1147             if (queryResults.getRow() == 0) {
1148                 return result; // return an empty list of document models
1149             }
1150
1151             // Otherwise, get the document IDs from the results of the query
1152             String id;
1153             queryResults.beforeFirst();
1154             while (queryResults.next()) {
1155                 id = queryResults.getString(1);
1156                 if (Tools.notBlank(id)) {
1157                     docIds.add(id);
1158                 }
1159             }
1160         } catch (SQLException sqle) {
1161             logger.warn("Could not obtain document IDs via SQL query '" + querySql + "': " + sqle.getMessage());
1162             return result; // return an empty list of document models
1163         } 
1164
1165         // Get a list of document models, using the list of IDs obtained from the query
1166         //
1167         // FIXME: Check whether we have a 'get document models from list of CSIDs'
1168         // utility method like this, and if not, add this to the appropriate
1169         // framework class
1170         DocumentModel docModel;
1171         for (String docId : docIds) {
1172             docModel = NuxeoUtils.getDocumentModel(repoSession, docId);
1173             if (docModel == null) {
1174                 logger.warn("Could not obtain document model for document with ID " + docId);
1175             } else {
1176                 result.add(NuxeoUtils.getDocumentModel(repoSession, docId));
1177             }
1178         }
1179         
1180         // Order the results
1181         final String COMMON_PART_SCHEMA = handler.getServiceContext().getCommonPartLabel();
1182         final String DISPLAY_NAME_XPATH =
1183                 "//" + handler.getJDBCQueryParams().get(TERM_GROUP_LIST_NAME) + "/[0]/termDisplayName";
1184         Collections.sort(result, new Comparator<DocumentModel>() {
1185             @Override
1186             public int compare(DocumentModel doc1, DocumentModel doc2) {
1187                 String termDisplayName1 = (String) NuxeoUtils.getXPathValue(doc1, COMMON_PART_SCHEMA, DISPLAY_NAME_XPATH);
1188                 String termDisplayName2 = (String) NuxeoUtils.getXPathValue(doc2, COMMON_PART_SCHEMA, DISPLAY_NAME_XPATH);
1189                 return termDisplayName1.compareToIgnoreCase(termDisplayName2);
1190             }
1191         });
1192
1193         return result;
1194     }
1195     
1196
1197     private DocumentModelList getFilteredCMIS(RepositoryInstance repoSession, ServiceContext ctx, DocumentHandler handler, QueryContext queryContext)
1198             throws DocumentNotFoundException, DocumentException {
1199
1200         DocumentModelList result = new DocumentModelListImpl();
1201         try {
1202             String query = handler.getCMISQuery(queryContext);
1203
1204             DocumentFilter docFilter = handler.getDocumentFilter();
1205             int pageSize = docFilter.getPageSize();
1206             int offset = docFilter.getOffset();
1207             if (logger.isDebugEnabled()) {
1208                 logger.debug("Executing CMIS query: " + query.toString()
1209                         + "with pageSize: " + pageSize + " at offset: " + offset);
1210             }
1211
1212             // If we have limit and/or offset, then pass true to get totalSize
1213             // in returned DocumentModelList.
1214             Profiler profiler = new Profiler(this, 2);
1215             profiler.log("Executing CMIS query: " + query.toString());
1216             profiler.start();
1217             //
1218             IterableQueryResult queryResult = makeCMISQLQuery(repoSession, query, queryContext);
1219             try {
1220                 int totalSize = (int) queryResult.size();
1221                 ((DocumentModelListImpl) result).setTotalSize(totalSize);
1222                 // Skip the rows before our offset
1223                 if (offset > 0) {
1224                     queryResult.skipTo(offset);
1225                 }
1226                 int nRows = 0;
1227                 for (Map<String, Serializable> row : queryResult) {
1228                     if (logger.isTraceEnabled()) {
1229                         logger.trace(" Hierarchy Table ID is:" + row.get(IQueryManager.CMIS_TARGET_NUXEO_ID)
1230                                 + " nuxeo:pathSegment is: " + row.get(IQueryManager.CMIS_TARGET_NAME));
1231                     }
1232                     String nuxeoId = (String) row.get(IQueryManager.CMIS_TARGET_NUXEO_ID);
1233                     DocumentModel docModel = NuxeoUtils.getDocumentModel(repoSession, nuxeoId);
1234                     result.add(docModel);
1235                     nRows++;
1236                     if (nRows >= pageSize && pageSize != 0) { // A page size of zero means that they want all of them
1237                         logger.debug("Got page full of items - quitting");
1238                         break;
1239                     }
1240                 }
1241             } finally {
1242                 queryResult.close();
1243             }
1244             //
1245             profiler.stop();
1246
1247         } catch (Exception e) {
1248             if (logger.isDebugEnabled()) {
1249                 logger.debug("Caught exception ", e);
1250             }
1251             throw new DocumentException(e);
1252         }
1253
1254         //
1255         // Since we're not supporting paging yet for CMIS queries, we need to perform
1256         // a workaround for the paging information we return in our list of results
1257         //
1258         /*
1259          if (result != null) {
1260          docFilter.setStartPage(0);
1261          if (totalSize > docFilter.getPageSize()) {
1262          docFilter.setPageSize(totalSize);
1263          ((DocumentModelListImpl)result).setTotalSize(totalSize);
1264          }
1265          }
1266          */
1267
1268         return result;
1269     }
1270
1271     private String logException(Exception e, String msg) {
1272         String result = null;
1273
1274         String exceptionMessage = e.getMessage();
1275         exceptionMessage = exceptionMessage != null ? exceptionMessage : "<No details provided>";
1276         result = msg = msg + ". Caught exception:" + exceptionMessage;
1277
1278         if (logger.isTraceEnabled() == true) {
1279             logger.error(msg, e);
1280         } else {
1281             logger.error(msg);
1282         }
1283
1284         return result;
1285     }
1286
1287     /**
1288      * update given document in the Nuxeo repository
1289      *
1290      * @param ctx service context under which this method is invoked
1291      * @param csid of the document
1292      * @param handler should be used by the caller to provide and transform the
1293      * document
1294      * @throws BadRequestException
1295      * @throws DocumentNotFoundException
1296      * @throws TransactionException if the transaction times out or otherwise
1297      * cannot be successfully completed
1298      * @throws DocumentException
1299      */
1300     @Override
1301     public void update(ServiceContext ctx, String csid, DocumentHandler handler)
1302             throws BadRequestException, DocumentNotFoundException, TransactionException,
1303             DocumentException {
1304         if (handler == null) {
1305             throw new IllegalArgumentException(
1306                     "RepositoryJavaClient.update: document handler is missing.");
1307         }
1308
1309         RepositoryInstance repoSession = null;
1310         try {
1311             handler.prepare(Action.UPDATE);
1312             repoSession = getRepositorySession(ctx);
1313             DocumentRef docRef = NuxeoUtils.createPathRef(ctx, csid);
1314             DocumentModel doc = null;
1315             try {
1316                 doc = repoSession.getDocument(docRef);
1317             } catch (ClientException ce) {
1318                 String msg = logException(ce, "Could not find document to update with CSID=" + csid);
1319                 throw new DocumentNotFoundException(msg, ce);
1320             }
1321             // Check for a versioned document, and check In and Out before we proceed.
1322             if (((DocumentModelHandler) handler).supportsVersioning()) {
1323                 /* Once we advance to 5.5 or later, we can add this. 
1324                  * See also https://jira.nuxeo.com/browse/NXP-8506
1325                  if(!doc.isVersionable()) {
1326                  throw new DocumentException("Configuration for: "
1327                  +handler.getServiceContextPath()+" supports versioning, but Nuxeo config does not!");
1328                  }
1329                  */
1330                 /* Force a version number - Not working. Apparently we need to configure the uid schema??
1331                  if(doc.getProperty("uid","major_version") == null) {
1332                  doc.setProperty("uid","major_version",1);
1333                  }
1334                  if(doc.getProperty("uid","minor_version") == null) {
1335                  doc.setProperty("uid","minor_version",0);
1336                  }
1337                  */
1338                 doc.checkIn(VersioningOption.MINOR, null);
1339                 doc.checkOut();
1340             }
1341
1342             //
1343             // Set reposession to handle the document
1344             //
1345             ((DocumentModelHandler) handler).setRepositorySession(repoSession);
1346             DocumentWrapper<DocumentModel> wrapDoc = new DocumentWrapperImpl<DocumentModel>(doc);
1347             handler.handle(Action.UPDATE, wrapDoc);
1348             repoSession.saveDocument(doc);
1349             repoSession.save();
1350             handler.complete(Action.UPDATE, wrapDoc);
1351         } catch (BadRequestException bre) {
1352             throw bre;
1353         } catch (DocumentException de) {
1354             throw de;
1355         } catch (CSWebApplicationException wae) {
1356             throw wae;
1357         } catch (Exception e) {
1358             if (logger.isDebugEnabled()) {
1359                 logger.debug("Caught exception ", e);
1360             }
1361             throw new DocumentException(e);
1362         } finally {
1363             if (repoSession != null) {
1364                 releaseRepositorySession(ctx, repoSession);
1365             }
1366         }
1367     }
1368
1369     /**
1370      * Save a documentModel to the Nuxeo repository.
1371      *
1372      * @param ctx service context under which this method is invoked
1373      * @param repoSession
1374      * @param docModel the document to save
1375      * @param fSaveSession if TRUE, will call CoreSession.save() to save
1376      * accumulated changes.
1377      * @throws ClientException
1378      * @throws DocumentException
1379      */
1380     public void saveDocWithoutHandlerProcessing(
1381             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1382             RepositoryInstance repoSession,
1383             DocumentModel docModel,
1384             boolean fSaveSession)
1385             throws ClientException, DocumentException {
1386
1387         try {
1388             repoSession.saveDocument(docModel);
1389             if (fSaveSession) {
1390                 repoSession.save();
1391             }
1392         } catch (ClientException ce) {
1393             throw ce;
1394         } catch (Exception e) {
1395             if (logger.isDebugEnabled()) {
1396                 logger.debug("Caught exception ", e);
1397             }
1398             throw new DocumentException(e);
1399         }
1400     }
1401
1402     /**
1403      * Save a list of documentModels to the Nuxeo repository.
1404      *
1405      * @param ctx service context under which this method is invoked
1406      * @param repoSession a repository session
1407      * @param docModelList a list of document models
1408      * @param fSaveSession if TRUE, will call CoreSession.save() to save
1409      * accumulated changes.
1410      * @throws ClientException
1411      * @throws DocumentException
1412      */
1413     public void saveDocListWithoutHandlerProcessing(
1414             ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx,
1415             RepositoryInstance repoSession,
1416             DocumentModelList docList,
1417             boolean fSaveSession)
1418             throws ClientException, DocumentException {
1419         try {
1420             DocumentModel[] docModelArray = new DocumentModel[docList.size()];
1421             repoSession.saveDocuments(docList.toArray(docModelArray));
1422             if (fSaveSession) {
1423                 repoSession.save();
1424             }
1425         } catch (ClientException ce) {
1426             throw ce;
1427         } catch (Exception e) {
1428             logger.error("Caught exception ", e);
1429             throw new DocumentException(e);
1430         }
1431     }
1432
1433     /**
1434      * delete a document from the Nuxeo repository
1435      *
1436      * @param ctx service context under which this method is invoked
1437      * @param id of the document
1438      * @throws DocumentException
1439      */
1440     @Override
1441     public void delete(ServiceContext ctx, String id, DocumentHandler handler) throws DocumentNotFoundException,
1442             DocumentException, TransactionException {
1443         if (ctx == null) {
1444             throw new IllegalArgumentException(
1445                     "delete(ctx, ix, handler): ctx is missing");
1446         }
1447         if (handler == null) {
1448             throw new IllegalArgumentException(
1449                     "delete(ctx, ix, handler): handler is missing");
1450         }
1451         if (logger.isDebugEnabled()) {
1452             logger.debug("Deleting document with CSID=" + id);
1453         }
1454         RepositoryInstance repoSession = null;
1455         try {
1456             handler.prepare(Action.DELETE);
1457             repoSession = getRepositorySession(ctx);
1458             DocumentWrapper<DocumentModel> wrapDoc = null;
1459             try {
1460                 DocumentRef docRef = NuxeoUtils.createPathRef(ctx, id);
1461                 wrapDoc = new DocumentWrapperImpl<DocumentModel>(repoSession.getDocument(docRef));
1462                 ((DocumentModelHandler) handler).setRepositorySession(repoSession);
1463                 handler.handle(Action.DELETE, wrapDoc);
1464                 repoSession.removeDocument(docRef);
1465             } catch (ClientException ce) {
1466                 String msg = logException(ce, "Could not find document to delete with CSID=" + id);
1467                 throw new DocumentNotFoundException(msg, ce);
1468             }
1469             repoSession.save();
1470             handler.complete(Action.DELETE, wrapDoc);
1471         } catch (DocumentException de) {
1472             throw de;
1473         } catch (Exception e) {
1474             if (logger.isDebugEnabled()) {
1475                 logger.debug("Caught exception ", e);
1476             }
1477             throw new DocumentException(e);
1478         } finally {
1479             if (repoSession != null) {
1480                 releaseRepositorySession(ctx, repoSession);
1481             }
1482         }
1483     }
1484
1485     /* (non-Javadoc)
1486      * @see org.collectionspace.services.common.storage.StorageClient#delete(org.collectionspace.services.common.context.ServiceContext, java.lang.String, org.collectionspace.services.common.document.DocumentHandler)
1487      */
1488     @Override
1489     @Deprecated
1490     public void delete(@SuppressWarnings("rawtypes") ServiceContext ctx, String id)
1491             throws DocumentNotFoundException, DocumentException {
1492         throw new UnsupportedOperationException();
1493         // Use the other delete instead
1494     }
1495
1496     @Override
1497     public Hashtable<String, String> retrieveWorkspaceIds(RepositoryDomainType repoDomain) throws Exception {
1498         return NuxeoConnectorEmbedded.getInstance().retrieveWorkspaceIds(repoDomain);
1499     }
1500
1501     @Override
1502     public String createDomain(RepositoryDomainType repositoryDomain) throws Exception {
1503         RepositoryInstance repoSession = null;
1504         String domainId = null;
1505         try {
1506             //
1507             // Open a connection to the domain's repo/db
1508             //
1509             String repoName = repositoryDomain.getRepositoryName();
1510             repoSession = getRepositorySession(repoName); // domainName=storageName=repoName=databaseName
1511             //
1512             // First create the top-level domain directory
1513             //
1514             String domainName = repositoryDomain.getStorageName();
1515             DocumentRef parentDocRef = new PathRef("/");
1516             DocumentModel parentDoc = repoSession.getDocument(parentDocRef);
1517             DocumentModel domainDoc = repoSession.createDocumentModel(parentDoc.getPathAsString(),
1518                     domainName, NUXEO_CORE_TYPE_DOMAIN);
1519             domainDoc.setPropertyValue("dc:title", domainName);
1520             domainDoc.setPropertyValue("dc:description", "A CollectionSpace domain "
1521                     + domainName);
1522             domainDoc = repoSession.createDocument(domainDoc);
1523             domainId = domainDoc.getId();
1524             repoSession.save();
1525             //
1526             // Next, create a "Workspaces" root directory to contain the workspace folders for the individual service documents
1527             //
1528             DocumentModel workspacesRoot = repoSession.createDocumentModel(domainDoc.getPathAsString(),
1529                     NuxeoUtils.Workspaces, NUXEO_CORE_TYPE_WORKSPACEROOT);
1530             workspacesRoot.setPropertyValue("dc:title", NuxeoUtils.Workspaces);
1531             workspacesRoot.setPropertyValue("dc:description", "A CollectionSpace workspaces directory for "
1532                     + domainDoc.getPathAsString());
1533             workspacesRoot = repoSession.createDocument(workspacesRoot);
1534             String workspacesRootId = workspacesRoot.getId();
1535             repoSession.save();
1536
1537             if (logger.isDebugEnabled()) {
1538                 logger.debug("Created tenant domain name=" + domainName
1539                         + " id=" + domainId + " "
1540                         + NuxeoUtils.Workspaces + " id=" + workspacesRootId);
1541                 logger.debug("Path to Domain: " + domainDoc.getPathAsString());
1542                 logger.debug("Path to Workspaces root: " + workspacesRoot.getPathAsString());
1543             }
1544         } catch (Exception e) {
1545             if (logger.isDebugEnabled()) {
1546                 logger.debug("Could not create tenant domain name=" + repositoryDomain.getStorageName() + " caught exception ", e);
1547             }
1548             throw e;
1549         } finally {
1550             if (repoSession != null) {
1551                 releaseRepositorySession(null, repoSession);
1552             }
1553         }
1554
1555         return domainId;
1556     }
1557
1558     @Override
1559     public String getDomainId(RepositoryDomainType repositoryDomain) throws Exception {
1560         String domainId = null;
1561         RepositoryInstance repoSession = null;
1562
1563         String repoName = repositoryDomain.getRepositoryName();
1564         String domainStorageName = repositoryDomain.getStorageName();
1565         if (domainStorageName != null && !domainStorageName.isEmpty()) {
1566             try {
1567                 repoSession = getRepositorySession(repoName);
1568                 DocumentRef docRef = new PathRef("/" + domainStorageName);
1569                 DocumentModel domain = repoSession.getDocument(docRef);
1570                 domainId = domain.getId();
1571             } catch (Exception e) {
1572                 if (logger.isTraceEnabled()) {
1573                     logger.trace("Caught exception ", e);  // The document doesn't exist, this let's us know we need to create it
1574                 }
1575                 //there is no way to identify if document does not exist due to
1576                 //lack of typed exception for getDocument method
1577                 return null;
1578             } finally {
1579                 if (repoSession != null) {
1580                     releaseRepositorySession(null, repoSession);
1581                 }
1582             }
1583         }
1584
1585         return domainId;
1586     }
1587
1588     /*
1589      * Returns the workspaces root directory for a given domain.
1590      */
1591     private DocumentModel getWorkspacesRoot(RepositoryInstance repoSession,
1592             String domainName) throws Exception {
1593         DocumentModel result = null;
1594
1595         String domainPath = "/" + domainName;
1596         DocumentRef parentDocRef = new PathRef(domainPath);
1597         DocumentModelList domainChildrenList = repoSession.getChildren(
1598                 parentDocRef);
1599         Iterator<DocumentModel> witer = domainChildrenList.iterator();
1600         while (witer.hasNext()) {
1601             DocumentModel childNode = witer.next();
1602             if (NuxeoUtils.Workspaces.equalsIgnoreCase(childNode.getName())) {
1603                 result = childNode;
1604                 logger.trace("Found workspaces directory at: " + result.getPathAsString());
1605                 break;
1606             }
1607         }
1608
1609         if (result == null) {
1610             throw new ClientException("Could not find workspace root directory in: "
1611                     + domainPath);
1612         }
1613
1614         return result;
1615     }
1616
1617     /* (non-Javadoc)
1618      * @see org.collectionspace.services.common.repository.RepositoryClient#createWorkspace(java.lang.String, java.lang.String)
1619      */
1620     @Override
1621     public String createWorkspace(RepositoryDomainType repositoryDomain, String workspaceName) throws Exception {
1622         RepositoryInstance repoSession = null;
1623         String workspaceId = null;
1624         try {
1625             String repoName = repositoryDomain.getRepositoryName();
1626             repoSession = getRepositorySession(repoName);
1627
1628             String domainStorageName = repositoryDomain.getStorageName();
1629             DocumentModel parentDoc = getWorkspacesRoot(repoSession, domainStorageName);
1630             if (logger.isTraceEnabled()) {
1631                 for (String facet : parentDoc.getFacets()) {
1632                     logger.trace("Facet: " + facet);
1633                 }
1634             }
1635
1636             DocumentModel doc = repoSession.createDocumentModel(parentDoc.getPathAsString(),
1637                     workspaceName, NuxeoUtils.WORKSPACE_DOCUMENT_TYPE);
1638             doc.setPropertyValue("dc:title", workspaceName);
1639             doc.setPropertyValue("dc:description", "A CollectionSpace workspace for "
1640                     + workspaceName);
1641             doc = repoSession.createDocument(doc);
1642             workspaceId = doc.getId();
1643             repoSession.save();
1644             if (logger.isDebugEnabled()) {
1645                 logger.debug("Created workspace name=" + workspaceName
1646                         + " id=" + workspaceId);
1647             }
1648         } catch (Exception e) {
1649             if (logger.isDebugEnabled()) {
1650                 logger.debug("createWorkspace caught exception ", e);
1651             }
1652             throw e;
1653         } finally {
1654             if (repoSession != null) {
1655                 releaseRepositorySession(null, repoSession);
1656             }
1657         }
1658         return workspaceId;
1659     }
1660
1661     /* (non-Javadoc)
1662      * @see org.collectionspace.services.common.repository.RepositoryClient#getWorkspaceId(java.lang.String, java.lang.String)
1663      */
1664     @Override
1665     @Deprecated
1666     public String getWorkspaceId(String tenantDomain, String workspaceName) throws Exception {
1667         String workspaceId = null;
1668
1669         RepositoryInstance repoSession = null;
1670         try {
1671             repoSession = getRepositorySession((ServiceContext) null);
1672             DocumentRef docRef = new PathRef(
1673                     "/" + tenantDomain
1674                     + "/" + NuxeoUtils.Workspaces
1675                     + "/" + workspaceName);
1676             DocumentModel workspace = repoSession.getDocument(docRef);
1677             workspaceId = workspace.getId();
1678         } catch (DocumentException de) {
1679             throw de;
1680         } catch (Exception e) {
1681             if (logger.isDebugEnabled()) {
1682                 logger.debug("Caught exception ", e);
1683             }
1684             throw new DocumentException(e);
1685         } finally {
1686             if (repoSession != null) {
1687                 releaseRepositorySession(null, repoSession);
1688             }
1689         }
1690
1691         return workspaceId;
1692     }
1693
1694     public RepositoryInstance getRepositorySession(ServiceContext ctx) throws Exception {
1695         return getRepositorySession(ctx, ctx.getRepositoryName());
1696     }
1697
1698     public RepositoryInstance getRepositorySession(String repoName) throws Exception {
1699         return getRepositorySession(null, repoName);
1700     }
1701
1702     /**
1703      * Gets the repository session. - Package access only. If the 'ctx' param is
1704      * null then the repo name must be non-mull and vice-versa
1705      *
1706      * @return the repository session
1707      * @throws Exception the exception
1708      */
1709     public RepositoryInstance getRepositorySession(ServiceContext ctx, String repoName) throws Exception {
1710         RepositoryInstance repoSession = null;
1711
1712         Profiler profiler = new Profiler("getRepositorySession():", 2);
1713         profiler.start();
1714         //
1715         // To get a connection to the Nuxeo repo, we need either a valid ServiceContext instance or a repository name
1716         //
1717         if (ctx != null) {
1718             repoName = ctx.getRepositoryName(); // Notice we are overriding the passed in 'repoName' since we have a valid service context passed in to us
1719             repoSession = (RepositoryInstance) ctx.getCurrentRepositorySession(); // Look to see if one exists in the context before creating one
1720         } else if (repoName == null || repoName.trim().isEmpty()) {
1721             String errMsg = String.format("We can't get a connection to the Nuxeo repo because the service context passed in was null and no repository name was passed in either.");
1722             logger.error(errMsg);
1723             throw new Exception(errMsg);
1724         }
1725         //
1726         // If we couldn't find a repoSession from the service context (or the context was null) then we need to create a new one using
1727         // just the repo name
1728         //
1729         if (repoSession == null) {
1730             NuxeoClientEmbedded client = NuxeoConnectorEmbedded.getInstance().getClient();
1731             repoSession = client.openRepository(repoName);
1732         } else {
1733             if (logger.isDebugEnabled() == true) {
1734                 logger.warn("Reusing the current context's repository session.");
1735             }
1736         }
1737
1738         try {
1739                 if (logger.isTraceEnabled()) {
1740                     logger.trace("Testing call to getRepository() repository root: " + repoSession.getRootDocument());
1741                 }
1742         } catch (Throwable e) {
1743                 logger.trace("Test call to Nuxeo's getRepository() repository root failed", e);
1744         }
1745
1746         profiler.stop();
1747
1748         if (ctx != null) {
1749             ctx.setCurrentRepositorySession(repoSession); // For reusing, save the repository session in the current service context
1750         }
1751
1752         return repoSession;
1753     }
1754
1755     /**
1756      * Release repository session. - Package access only.
1757      *
1758      * @param repoSession the repo session
1759      */
1760     public void releaseRepositorySession(ServiceContext ctx, RepositoryInstance repoSession) throws TransactionException {
1761         try {
1762             NuxeoClientEmbedded client = NuxeoConnectorEmbedded.getInstance().getClient();
1763             // release session
1764             if (ctx != null) {
1765                 ctx.clearCurrentRepositorySession(); //clear the current context of the now closed repo session
1766                 if (ctx.getCurrentRepositorySession() == null) {
1767                     client.releaseRepository(repoSession); //release the repo session if the service context's ref count is zeo.
1768                 }
1769             } else {
1770                 client.releaseRepository(repoSession); //repo session was acquired without a service context
1771             }
1772         } catch (TransactionRuntimeException tre) {
1773             TransactionException te = new TransactionException(tre);
1774             logger.error(te.getMessage(), tre); // Log the standard transaction exception message, plus an exception-specific stack trace
1775             throw te;
1776         } catch (Exception e) {
1777             logger.error("Could not close the repository session", e);
1778             // no need to throw this service specific exception
1779         }
1780     }
1781
1782     @Override
1783     public void doWorkflowTransition(ServiceContext ctx, String id,
1784             DocumentHandler handler, TransitionDef transitionDef)
1785             throws BadRequestException, DocumentNotFoundException,
1786             DocumentException {
1787         // This is a placeholder for when we change the StorageClient interface to treat workflow transitions as 1st class operations like 'get', 'create', 'update, 'delete', etc
1788     }
1789
1790     private String handleProvidedStartingWildcard(String partialTerm) {
1791         if (Tools.notBlank(partialTerm)) {
1792             if (partialTerm.substring(0, 1).equals(USER_SUPPLIED_WILDCARD)) {
1793                 StringBuffer buffer = new StringBuffer(partialTerm);
1794                 buffer.setCharAt(0, JDBCTools.SQL_WILDCARD.charAt(0));
1795                 partialTerm = buffer.toString();
1796             }
1797         }
1798         return partialTerm;
1799     }
1800     
1801     /**
1802      * Replaces user-supplied wildcards with SQL wildcards, in a partial term
1803      * matching search expression.
1804      * 
1805      * The scope of this replacement excludes the beginning character
1806      * in that search expression, as that character is treated specially.
1807      * 
1808      * @param partialTerm
1809      * @return the partial term, with any user-supplied wildcards replaced
1810      * by SQL wildcards.
1811      */
1812     private String subtituteWildcardsInPartialTerm(String partialTerm) {
1813         if (Tools.isBlank(partialTerm)) {
1814             return partialTerm;
1815         }
1816         if (! partialTerm.contains(USER_SUPPLIED_WILDCARD)) {
1817             return partialTerm;
1818         }
1819         int len = partialTerm.length();
1820         // Partial term search expressions of 2 or fewer characters
1821         // currently aren't amenable to the use of wildcards
1822         if (len <= 2)  {
1823             logger.warn("Partial term match search expression of just 1-2 characters in length contains a user-supplied wildcard: " + partialTerm);
1824             logger.warn("Will handle that character as a literal value, rather than as a wildcard ...");
1825             return partialTerm;
1826         }
1827         return partialTerm.substring(0, 1) // first char
1828                 + partialTerm.substring(1, len).replaceAll(USER_SUPPLIED_WILDCARD_REGEX, JDBCTools.SQL_WILDCARD);
1829
1830     }
1831
1832     private int getMaxItemsLimitOnJdbcQueries(String maxListItemsLimit) {
1833         final int DEFAULT_ITEMS_LIMIT = 40;
1834         if (maxListItemsLimit == null) {
1835             return DEFAULT_ITEMS_LIMIT;
1836         }
1837         int itemsLimit;
1838         try {
1839             itemsLimit = Integer.parseInt(maxListItemsLimit);
1840             if (itemsLimit < 1) {
1841                 logger.warn("Value of configuration setting "
1842                         + IQueryManager.MAX_LIST_ITEMS_RETURNED_LIMIT_ON_JDBC_QUERIES
1843                         + " must be a positive integer; invalid current value is " + maxListItemsLimit);
1844                 logger.warn("Reverting to default value of " + DEFAULT_ITEMS_LIMIT);
1845                 itemsLimit = DEFAULT_ITEMS_LIMIT;
1846             }
1847         } catch (NumberFormatException nfe) {
1848             logger.warn("Value of configuration setting "
1849                         + IQueryManager.MAX_LIST_ITEMS_RETURNED_LIMIT_ON_JDBC_QUERIES
1850                         + " must be a positive integer; invalid current value is " + maxListItemsLimit);
1851             logger.warn("Reverting to default value of " + DEFAULT_ITEMS_LIMIT);
1852             itemsLimit = DEFAULT_ITEMS_LIMIT;
1853         }
1854         return itemsLimit;
1855     }
1856
1857     /**
1858      * Identifies whether a restriction on tenant ID - to return only records
1859      * pertaining to the current tenant - is required in a JDBC query.
1860      * 
1861      * @param tenantBinding a tenant binding configuration.
1862      * @param ctx a service context.
1863      * @return true if a restriction on tenant ID is required in the query;
1864      * false if a restriction is not required.
1865      */
1866     private boolean restrictJDBCQueryByTenantID(TenantBindingType tenantBinding, ServiceContext ctx) {
1867         boolean restrict = true;
1868         // If data for the current service, in the current tenant, is isolated
1869         // within its own separate, per-tenant repository, as contrasted with
1870         // being intermingled with other tenants' data in the default repository,
1871         // no restriction on Tenant ID is required in the query.
1872         String repositoryDomainName = ConfigUtils.getRepositoryName(tenantBinding, ctx.getRepositoryDomainName());
1873         if (!(repositoryDomainName.equals(ConfigUtils.DEFAULT_NUXEO_REPOSITORY_NAME))) {
1874             restrict = false;
1875         }
1876         // If a configuration setting for this tenant identifies that JDBC
1877         // queries should not be restricted by tenant ID (perhaps because
1878         // there is always expected to be only one tenant's data present in
1879         // the system), no restriction on Tenant ID is required in the query.
1880         String queriesRestrictedByTenantId = TenantBindingUtils.getPropertyValue(tenantBinding,
1881                 IQueryManager.JDBC_QUERIES_ARE_TENANT_ID_RESTRICTED);
1882         if (Tools.notBlank(queriesRestrictedByTenantId) &&
1883                 queriesRestrictedByTenantId.equalsIgnoreCase(Boolean.FALSE.toString())) {
1884             restrict = false;
1885         }
1886         return restrict;
1887     }
1888 }