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