]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
c5970e51c5bf0640c17d90f528e36b01b8c55484
[tmp/jakarta-migration.git] /
1 /**
2  *  This document is a part of the source code and related artifacts
3  *  for CollectionSpace, an open source collections management system
4  *  for museums and related institutions:
5
6  *  http://www.collectionspace.org
7  *  http://wiki.collectionspace.org
8
9  *  Copyright 2009 University of California at Berkeley
10
11  *  Licensed under the Educational Community License (ECL), Version 2.0.
12  *  You may not use this file except in compliance with this License.
13
14  *  You may obtain a copy of the ECL 2.0 License at
15
16  *  https://source.collectionspace.org/collection-space/LICENSE.txt
17
18  *  Unless required by applicable law or agreed to in writing, software
19  *  distributed under the License is distributed on an "AS IS" BASIS,
20  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21  *  See the License for the specific language governing permissions and
22  *  limitations under the License.
23  */
24 package org.collectionspace.services.nuxeo.client.java;
25
26 import java.util.Collection;
27 import java.util.List;
28
29 import javax.ws.rs.core.MultivaluedMap;
30
31 import org.apache.commons.lang.StringUtils;
32 import org.collectionspace.services.client.Profiler;
33 import org.collectionspace.services.client.CollectionSpaceClient;
34 import org.collectionspace.services.client.IClientQueryParams;
35 import org.collectionspace.services.client.IQueryManager;
36 import org.collectionspace.services.client.IRelationsManager;
37 import org.collectionspace.services.client.PoxPayloadIn;
38 import org.collectionspace.services.client.PoxPayloadOut;
39 import org.collectionspace.services.common.api.CommonAPI;
40 import org.collectionspace.services.common.api.GregorianCalendarDateTimeUtils;
41 import org.collectionspace.services.common.api.RefName;
42 import org.collectionspace.services.common.api.RefName.RefNameInterface;
43 import org.collectionspace.services.common.api.RefNameUtils;
44 import org.collectionspace.services.common.api.Tools;
45 import org.collectionspace.services.common.authorityref.AuthorityRefList;
46 import org.collectionspace.services.common.context.ServiceContext;
47 import org.collectionspace.services.common.document.AbstractMultipartDocumentHandlerImpl;
48 import org.collectionspace.services.common.document.DocumentException;
49 import org.collectionspace.services.common.document.DocumentFilter;
50 import org.collectionspace.services.common.document.DocumentNotFoundException;
51 import org.collectionspace.services.common.document.DocumentWrapper;
52 import org.collectionspace.services.common.document.DocumentWrapperImpl;
53 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
54 import org.collectionspace.services.common.query.QueryContext;
55 import org.collectionspace.services.common.repository.RepositoryClient;
56 import org.collectionspace.services.common.repository.RepositoryClientFactory;
57 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.AuthRefConfigInfo;
58 import org.collectionspace.services.common.vocabulary.RefNameServiceUtils.Specifier;
59 import org.collectionspace.services.lifecycle.Lifecycle;
60 import org.collectionspace.services.lifecycle.State;
61 import org.collectionspace.services.lifecycle.StateList;
62 import org.collectionspace.services.lifecycle.TransitionDef;
63 import org.collectionspace.services.lifecycle.TransitionDefList;
64 import org.collectionspace.services.lifecycle.TransitionList;
65 import org.nuxeo.ecm.core.NXCore;
66 import org.nuxeo.ecm.core.api.ClientException;
67 import org.nuxeo.ecm.core.api.DocumentModel;
68 import org.nuxeo.ecm.core.api.DocumentModelList;
69 import org.nuxeo.ecm.core.api.model.PropertyException;
70 import org.nuxeo.ecm.core.lifecycle.LifeCycle;
71 import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * DocumentModelHandler is a base abstract Nuxeo document handler
77  * using Nuxeo Java Remote APIs for CollectionSpace services
78  *
79  * $LastChangedRevision: $
80  * $LastChangedDate: $
81  */
82 public abstract class DocumentModelHandler<T, TL>
83         extends AbstractMultipartDocumentHandlerImpl<T, TL, DocumentModel, DocumentModelList> {
84
85     private final Logger logger = LoggerFactory.getLogger(DocumentModelHandler.class);
86     private CoreSessionInterface repositorySession;
87
88     protected String oldRefNameOnUpdate = null;  // FIXME: REM - We should have setters and getters for these
89     protected String newRefNameOnUpdate = null;  // FIXME: two fields.
90     
91     
92     /*
93      * Returns the the life cycle definition of the related Nuxeo document type for this handler.
94      * (non-Javadoc)
95      * @see org.collectionspace.services.common.document.DocumentHandler#getLifecycle()
96      */
97     @Override
98     public Lifecycle getLifecycle() {
99         Lifecycle result = null;
100         
101         String docTypeName = null;
102         try {
103                 docTypeName = this.getServiceContext().getTenantQualifiedDoctype();
104                 result = getLifecycle(docTypeName);
105                 if (result == null) {
106                     //
107                     // Get the lifecycle of the generic type if one for the tenant qualified type doesn't exist
108                     //
109             docTypeName = this.getServiceContext().getDocumentType();
110             result = getLifecycle(docTypeName);
111                 }
112         } catch (Exception e) {
113                 if (logger.isTraceEnabled() == true) {
114                         logger.trace("Could not retrieve lifecycle definition for Nuxeo doctype: " + docTypeName);
115                 }
116         }
117         
118             return result;
119     }
120     
121     /*
122      * Returns the the life cycle definition of the related Nuxeo document type for this handler.
123      * (non-Javadoc)
124      * @see org.collectionspace.services.common.document.DocumentHandler#getLifecycle(java.lang.String)
125      */
126     @Override
127     public Lifecycle getLifecycle(String docTypeName) {         
128         return NuxeoUtils.getLifecycle(docTypeName);
129     }
130     
131     /*
132      * We're using the "name" field of Nuxeo's DocumentModel to store
133      * the CSID.
134      */
135     public String getCsid(DocumentModel docModel) {
136         return NuxeoUtils.getCsid(docModel);
137     }
138
139     public String getUri(DocumentModel docModel) {
140         return getServiceContextPath()+getCsid(docModel);
141     }
142     
143     public String getUri(Specifier specifier) {
144         return getServiceContextPath() + specifier.value;
145     }
146     
147         
148     public RepositoryClient<PoxPayloadIn, PoxPayloadOut> getRepositoryClient(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx) {
149         RepositoryClient<PoxPayloadIn, PoxPayloadOut> repositoryClient = 
150                         (RepositoryClient<PoxPayloadIn, PoxPayloadOut>) RepositoryClientFactory.getInstance().getClient(ctx.getRepositoryClientName());
151         return repositoryClient;
152     }
153
154     /**
155      * getRepositorySession returns Nuxeo Repository Session
156      * @return
157      */
158     public CoreSessionInterface getRepositorySession() {
159         
160         return repositorySession;
161     }
162
163     /**
164      * setRepositorySession sets repository session
165      * @param repoSession
166      */
167     public void setRepositorySession(CoreSessionInterface repoSession) {
168         this.repositorySession = repoSession;
169     }
170
171     @Override
172     public void handleCreate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
173         // TODO for sub-docs - check to see if the current service context is a multipart input, 
174         // OR a docfragment, and call a variant to fill the DocModel.
175         fillAllParts(wrapDoc, Action.CREATE);
176         handleCoreValues(wrapDoc, Action.CREATE);
177     }
178     
179     // TODO for sub-docs - Add completeCreate in which we look for set-aside doc fragments 
180     // and create the subitems. We will create service contexts with the doc fragments
181     // and then call 
182
183
184     @Override
185     public void handleUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
186         // TODO for sub-docs - check to see if the current service context is a multipart input, 
187         // OR a docfragment, and call a variant to fill the DocModel.
188         fillAllParts(wrapDoc, Action.UPDATE);
189         handleCoreValues(wrapDoc, Action.UPDATE);
190     }
191
192     @Override
193     public void handleGet(DocumentWrapper<DocumentModel> wrapDoc) throws Exception {
194                 extractAllParts(wrapDoc);
195     }
196
197     @Override
198     public void handleGetAll(DocumentWrapper<DocumentModelList> wrapDoc) throws Exception {
199         Profiler profiler = new Profiler(this, 2);
200         profiler.start();
201         setCommonPartList(extractCommonPartList(wrapDoc));
202         profiler.stop();
203     }
204
205     @Override
206     public abstract void completeUpdate(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
207
208     @Override
209     public abstract void extractAllParts(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
210
211     @Override
212     public abstract T extractCommonPart(DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
213
214     @Override
215     public abstract void fillAllParts(DocumentWrapper<DocumentModel> wrapDoc, Action action) throws Exception;
216
217     @Override
218     public abstract void fillCommonPart(T obj, DocumentWrapper<DocumentModel> wrapDoc) throws Exception;
219
220     @Override
221     public abstract TL extractCommonPartList(DocumentWrapper<DocumentModelList> wrapDoc) throws Exception;
222
223     @Override
224     public abstract T getCommonPart();
225
226     @Override
227     public abstract void setCommonPart(T obj);
228
229     @Override
230     public abstract TL getCommonPartList();
231
232     @Override
233     public abstract void setCommonPartList(TL obj);
234     
235     @Override
236     public DocumentFilter createDocumentFilter() {
237         DocumentFilter filter = new NuxeoDocumentFilter(this.getServiceContext());
238         return filter;
239     }
240     
241     /**
242      * Gets the authority refs.
243      *
244      * @param docWrapper the doc wrapper
245      * @param authRefFields the auth ref fields
246      * @return the authority refs
247      * @throws PropertyException the property exception
248      */
249     abstract public AuthorityRefList getAuthorityRefs(String csid,
250                 List<AuthRefConfigInfo> authRefConfigInfoList) throws PropertyException, Exception;    
251
252     /*
253      * Subclasses should override this method if they need to customize their refname generation
254      */
255     protected RefName.RefNameInterface getRefName(ServiceContext ctx,
256                 DocumentModel docModel) {
257         return getRefName(new DocumentWrapperImpl<DocumentModel>(docModel), ctx.getTenantName(), ctx.getServiceName());
258     }
259     
260     /*
261      * By default, we'll use the CSID as the short ID.  Sub-classes can override this method if they want to use
262      * something else for a short ID.
263      * 
264      * (non-Javadoc)
265      * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#getRefName(org.collectionspace.services.common.document.DocumentWrapper, java.lang.String, java.lang.String)
266      */
267     @Override
268         protected RefName.RefNameInterface getRefName(DocumentWrapper<DocumentModel> docWrapper,
269                         String tenantName, String serviceName) {
270         String csid = docWrapper.getWrappedObject().getName();
271         String refnameDisplayName = getRefnameDisplayName(docWrapper);
272         RefName.RefNameInterface refname = RefName.Authority.buildAuthority(tenantName, serviceName,
273                         csid, null, refnameDisplayName);
274         return refname;
275         }
276
277     private void handleCoreValues(DocumentWrapper<DocumentModel> docWrapper, 
278                 Action action)  throws ClientException {
279         DocumentModel documentModel = docWrapper.getWrappedObject();
280         String now = GregorianCalendarDateTimeUtils.timestampUTC();
281         ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx = getServiceContext();
282         String userId = ctx.getUserId();
283         if (action == Action.CREATE) {
284             //
285             // Add the tenant ID value to the new entity
286             //
287                 String tenantId = ctx.getTenantId();
288             documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
289                         CollectionSpaceClient.COLLECTIONSPACE_CORE_TENANTID, tenantId);
290             //
291             // Add the uri value to the new entity
292             //
293             documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
294                         CollectionSpaceClient.COLLECTIONSPACE_CORE_URI, getUri(documentModel));
295                 //
296                 // Add the CSID to the DublinCore title so we can see the CSID in the default
297                 // Nuxeo webapp.
298                 //
299                 try {
300                 documentModel.setProperty(CommonAPI.NUXEO_DUBLINCORE_SCHEMANAME, CommonAPI.NUXEO_DUBLINCORE_TITLE,
301                         documentModel.getName());
302                 } catch (Exception x) {
303                         if (logger.isWarnEnabled() == true) {
304                                 logger.warn("Could not set the Dublin Core 'title' field on document CSID:" +
305                                                 documentModel.getName());
306                         }
307                 }
308                 //
309                 // Add createdAt timestamp and createdBy user
310                 //
311             documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
312                         CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_AT, now);
313             documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
314                         CollectionSpaceClient.COLLECTIONSPACE_CORE_CREATED_BY, userId);
315         }
316         
317                 if (action == Action.CREATE || action == Action.UPDATE) {
318             //
319             // Add/update the resource's refname
320             //
321                         handleRefNameChanges(ctx, documentModel);
322             //
323             // Add updatedAt timestamp and updateBy user
324             //
325                         if (ctx.shouldUpdateCoreValues() == true) { // Ensure that our caller wants us to record this update
326                                 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
327                                                 CollectionSpaceClient.COLLECTIONSPACE_CORE_UPDATED_AT, now);
328                                 documentModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
329                                                 CollectionSpaceClient.COLLECTIONSPACE_CORE_UPDATED_BY, userId);
330                         } else {
331                                 logger.debug(String.format("Document with CSID=%s updated %s by user %s", documentModel.getName(), now, userId));
332                         }
333                 }               
334     }
335     
336     protected boolean hasRefNameUpdate() {
337         boolean result = false;
338         
339         //
340         // Check to see if the request contains a query parameter asking us to force a refname update
341         //
342         if (getServiceContext().shouldForceUpdateRefnameReferences() == true) {
343                 return true;
344         }
345         
346         if (Tools.notBlank(newRefNameOnUpdate) && Tools.notBlank(oldRefNameOnUpdate)) {
347                 // CSPACE-6372: refNames are different if:
348                 //   - any part of the refName is different, using a case insensitive comparison, or
349                 //   - the display name portions are different, using a case sensitive comparison
350                 if (newRefNameOnUpdate.equalsIgnoreCase(oldRefNameOnUpdate) == false) {
351                         result = true; // refNames are different so updates are needed
352                 }
353                 else {
354                         String newDisplayNameOnUpdate = RefNameUtils.getDisplayName(newRefNameOnUpdate);
355                         String oldDisplayNameOnUpdate = RefNameUtils.getDisplayName(oldRefNameOnUpdate);
356                         
357                         if (StringUtils.equals(newDisplayNameOnUpdate, oldDisplayNameOnUpdate) == false) {
358                                 result = true; // display names are different so updates are needed
359                         }
360                 }
361         }
362         
363         return result;
364     }
365     
366     protected void handleRefNameChanges(ServiceContext<PoxPayloadIn, PoxPayloadOut> ctx, DocumentModel docModel) throws ClientException {
367         // First get the old refName
368         this.oldRefNameOnUpdate = (String)docModel.getProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
369                         CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME);
370         // Next, get the new refName
371         RefNameInterface refName = getRefName(ctx, docModel); // Sub-classes may override the getRefName() method called here.
372         if (refName != null) {
373                 this.newRefNameOnUpdate = refName.toString();
374         } else {
375                 logger.error(String.format("The refName for document is missing.  Document CSID=%s", docModel.getName())); // FIXME: REM - We should probably be throwing an exception here?
376         }
377         //
378         // Set the refName if it is an update or if the old refName was empty or null
379         //
380         if (hasRefNameUpdate() == true || this.oldRefNameOnUpdate == null) {
381                 docModel.setProperty(CollectionSpaceClient.COLLECTIONSPACE_CORE_SCHEMA,
382                                 CollectionSpaceClient.COLLECTIONSPACE_CORE_REFNAME, this.newRefNameOnUpdate);
383         }
384     }
385         
386     /*
387      * If we see the "rtSbj" query param then we need to perform a CMIS query.  Currently, we have only one
388      * CMIS query, but we could add more.  If we do, this method should look at the incoming request and corresponding
389      * query params to determine if we need to do a CMIS query
390      * (non-Javadoc)
391      * @see org.collectionspace.services.common.document.AbstractDocumentHandlerImpl#isCMISQuery()
392      */
393     public boolean isCMISQuery() {
394         boolean result = false;
395         
396         MultivaluedMap<String, String> queryParams = getServiceContext().getQueryParams();
397         //
398         // Look the query params to see if we need to make a CMSIS query.
399         //
400         String asSubjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_SUBJECT);           
401         String asOjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_OBJECT);      
402         String asEither = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_EITHER);         
403         if (asSubjectCsid != null || asOjectCsid != null || asEither != null) {
404                 result = true;
405         }
406         
407         return result;
408     }
409     
410     @Override
411     public String getDocumentsToIndexQuery(String indexId, String csid) throws DocumentException, Exception {
412         String result = null;
413         
414         ServiceContext<PoxPayloadIn,PoxPayloadOut> ctx = this.getServiceContext();
415         String selectClause = "SELECT ecm:uuid, ecm:primaryType FROM ";
416         String docFilterWhereClause = this.getDocumentFilter().getWhereClause();
417         //
418         // The where clause could be a combination of the document filter's where clause plus a CSID qualifier
419         //
420         String whereClause = (csid == null) ? null : String.format("ecm:name = '%s'", csid); // AND ecm:currentLifeCycleState <> 'deleted'"
421         if (whereClause != null && !whereClause.trim().isEmpty()) {
422             // Due to an apparent bug/issue in how Nuxeo translates the NXQL query string
423             // into SQL, we need to parenthesize our 'where' clause
424                 if (docFilterWhereClause != null && !docFilterWhereClause.trim().isEmpty()) {
425                         whereClause = whereClause + IQueryManager.SEARCH_QUALIFIER_AND + "(" + docFilterWhereClause + ")";
426                 }
427         } else {
428                 whereClause = docFilterWhereClause;
429         }
430         String orderByClause = "ecm:uuid";
431         
432         try {
433                 QueryContext queryContext = new QueryContext(ctx, selectClause, whereClause, orderByClause);
434                 result = NuxeoUtils.buildNXQLQuery(queryContext);
435         } catch (DocumentException de) {
436                 throw de;
437         } catch (Exception x) {
438                 throw x;
439         }
440
441         return result;
442     }
443     
444         /**
445          * Creates the CMIS query from the service context. Each document handler is
446          * responsible for returning (can override) a valid CMIS query using the information in the
447          * current service context -which includes things like the query parameters,
448          * etc.
449          * 
450          * This method implementation supports three mutually exclusive cases. We will build a query
451          * that can find a document(s) 'A' in a relationship with another document
452          * 'B' where document 'B' has a CSID equal to the query param passed in and:
453          *              1. Document 'B' is the subject of the relationship
454          *              2. Document 'B' is the object of the relationship
455          *              3. Document 'B' is either the object or the subject of the relationship
456          * @throws DocumentException 
457          */
458     @Override
459     public String getCMISQuery(QueryContext queryContext) throws DocumentException {
460         String result = null;
461         
462         if (isCMISQuery() == true) {
463                 //
464                 // Build up the query arguments
465                 //
466                 String theOnClause = "";
467                 String theWhereClause = "";
468                 MultivaluedMap<String, String> queryParams = getServiceContext().getQueryParams();
469                 String asSubjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_SUBJECT);
470                 String asObjectCsid = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_TO_CSID_AS_OBJECT);
471                 
472                 String matchObjDocTypes = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_MATCH_OBJ_DOCTYPES);
473                 String selectDocType = (String)queryParams.getFirst(IQueryManager.SELECT_DOC_TYPE_FIELD);
474
475                 String docType = NuxeoUtils.getTenantQualifiedDocType(this.getServiceContext()); // Fixed for https://issues.collectionspace.org/browse/DRYD-302
476                 if (selectDocType != null && !selectDocType.isEmpty()) {
477                         docType = selectDocType;
478                 }
479                 String selectFields = IQueryManager.CMIS_TARGET_CSID + ", "
480                                 + IQueryManager.CMIS_TARGET_TITLE + ", "
481                                 + IRelationsManager.CMIS_CSPACE_RELATIONS_TITLE + ", "
482                                 + IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_ID + ", "
483                                 + IRelationsManager.CMIS_CSPACE_RELATIONS_SUBJECT_ID;
484
485                 String targetTable = docType + " " + IQueryManager.CMIS_TARGET_PREFIX;
486                 String relTable = IRelationsManager.DOC_TYPE + " " + IQueryManager.CMIS_RELATIONS_PREFIX;
487                 
488                 String relSubjectCsidCol = IRelationsManager.CMIS_CSPACE_RELATIONS_SUBJECT_ID;
489                 String relObjectCsidCol = IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_ID;
490                 
491                 String targetCsidCol = IQueryManager.CMIS_TARGET_CSID;
492                 String tenantID = this.getServiceContext().getTenantId();
493
494                 //
495                 // Create the "ON" and "WHERE" query clauses based on the params passed into the HTTP request.  
496                 //
497                 // First come, first serve -the first match determines the "ON" and "WHERE" query clauses.
498                 //
499                 if (asSubjectCsid != null && !asSubjectCsid.isEmpty()) {  
500                         // Since our query param is the "subject" value, join the tables where the CSID of the document is the other side (the "object") of the relationship.
501                         theOnClause = relObjectCsidCol + " = " + targetCsidCol;
502                         theWhereClause = relSubjectCsidCol + " = " + "'" + asSubjectCsid + "'";
503                 } else if (asObjectCsid != null && !asObjectCsid.isEmpty()) {
504                         // Since our query param is the "object" value, join the tables where the CSID of the document is the other side (the "subject") of the relationship.
505                         theOnClause = relSubjectCsidCol + " = " + targetCsidCol; 
506                         theWhereClause = relObjectCsidCol + " = " + "'" + asObjectCsid + "'";
507                 } else {
508                         //Since the call to isCMISQuery() return true, we should never get here.
509                         logger.error("Attempt to make CMIS query failed because the HTTP request was missing valid query parameters.");
510                 }
511                 
512                 // Now consider a constraint on the object doc types (for search by service group)
513                 if (matchObjDocTypes != null && !matchObjDocTypes.isEmpty()) {  
514                         // Since our query param is the "subject" value, join the tables where the CSID of the document is the other side (the "object") of the relationship.
515                         theWhereClause += " AND (" + IRelationsManager.CMIS_CSPACE_RELATIONS_OBJECT_TYPE 
516                                                                 + " IN " + matchObjDocTypes + ")";
517                 }
518                 
519                 // Qualify the search for predicate types
520                 theWhereClause = addWhereClauseForPredicates(theWhereClause, queryParams);
521                 
522                 // Qualify the query with the current tenant ID.
523                 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IQueryManager.CMIS_JOIN_TENANT_ID_FILTER + " = '" + tenantID + "'";
524                 
525                 // This could later be in control of a queryParam, to omit if we want to see versions, or to
526                 // only see old versions.
527                 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IQueryManager.CMIS_JOIN_NUXEO_IS_VERSION_FILTER;
528                 
529                 StringBuilder query = new StringBuilder();
530                 // assemble the query from the string arguments
531                 query.append("SELECT ");
532                 query.append(selectFields);
533                 query.append(" FROM " + targetTable + " JOIN " + relTable);
534                 query.append(" ON " + theOnClause);
535                 query.append(" WHERE " + theWhereClause);
536                 
537                 try {
538                                 NuxeoUtils.appendCMISOrderBy(query, queryContext);
539                         } catch (Exception e) {
540                                 logger.error("Could not append ORDER BY clause to CMIS query", e);
541                         }
542                 
543                 // An example:
544                 // SELECT D.cmis:name, D.dc:title, R.dc:title, R.relations_common:subjectCsid
545                 // FROM Dimension D JOIN Relation R
546                 // ON R.relations_common:objectCsid = D.cmis:name
547                 // WHERE R.relations_common:subjectCsid = '737527ec-a560-4776-99de'
548                 // ORDER BY D.collectionspace_core:updatedAt DESC
549                 
550                 result = query.toString();
551                 if (logger.isDebugEnabled() == true && result != null) {
552                         logger.debug("The CMIS query is: " + result);
553                 }
554         }
555         
556         return result;
557     }
558
559         private String addWhereClauseForPredicates(String theWhereClause, MultivaluedMap<String, String> queryParams) {
560                 if (queryParams.containsKey(IQueryManager.SEARCH_RELATED_PREDICATE)) {
561                         List<String> predicateList = queryParams.get(IQueryManager.SEARCH_RELATED_PREDICATE);
562                         
563                         if (predicateList.size() == 1) {
564                         String predicate = (String)queryParams.getFirst(IQueryManager.SEARCH_RELATED_PREDICATE);
565                         if (predicate != null && !predicate.trim().isEmpty()) {
566                                 theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IRelationsManager.CMIS_CSPACE_RELATIONS_PREDICATE + " = '" + predicate + "'";
567                         }
568                         } else if (predicateList.size() > 1) {
569                                 StringBuffer partialClause = new StringBuffer();
570                                 for (String predicate : predicateList) {
571                                         if (!predicate.trim().isEmpty()) {
572                                                 partialClause.append("'" + predicate + "', ");
573                                         }
574                                 }
575                                 String inValues = partialClause.toString().replaceAll(", $", ""); // remove the last ', ' squence
576                                 if (!inValues.trim().isEmpty()) {
577                                         theWhereClause += IQueryManager.SEARCH_QUALIFIER_AND + IRelationsManager.CMIS_CSPACE_RELATIONS_PREDICATE + " IN (" + inValues + ")";
578                                 }
579                         }
580                 }
581                 
582                 return theWhereClause;
583         }
584     
585 }