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