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