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