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