1 | /** |
---|
2 | * Search Domain Class |
---|
3 | * |
---|
4 | * Abstract class containing search criteria and search results when querying. |
---|
5 | * Should be subclassed in order to enable searching for different entities. |
---|
6 | * |
---|
7 | * @author Robert Horlings (robert@isdat.nl) |
---|
8 | * @since 20110118 |
---|
9 | * @package dbnp.query |
---|
10 | * |
---|
11 | * Revision information: |
---|
12 | * $Rev: 1966 $ |
---|
13 | * $Author: work@osx.eu $ |
---|
14 | * $Date: 2011-07-14 15:03:06 +0000 (do, 14 jul 2011) $ |
---|
15 | */ |
---|
16 | package dbnp.query |
---|
17 | |
---|
18 | import org.dbnp.gdt.* |
---|
19 | import java.util.List; |
---|
20 | import java.text.DateFormat; |
---|
21 | import java.text.SimpleDateFormat |
---|
22 | import java.util.List; |
---|
23 | |
---|
24 | import org.springframework.context.ApplicationContext |
---|
25 | import org.springframework.web.context.request.RequestContextHolder; |
---|
26 | import org.codehaus.groovy.grails.commons.ApplicationHolder; |
---|
27 | |
---|
28 | import dbnp.authentication.* |
---|
29 | |
---|
30 | /** |
---|
31 | * Available boolean operators for searches |
---|
32 | * @author robert |
---|
33 | * |
---|
34 | */ |
---|
35 | enum SearchMode { |
---|
36 | and, or |
---|
37 | } |
---|
38 | |
---|
39 | class Search { |
---|
40 | /** |
---|
41 | * User that is performing this search. This has impact on the search results returned. |
---|
42 | */ |
---|
43 | public SecUser user; |
---|
44 | |
---|
45 | /** |
---|
46 | * Date of execution of this search |
---|
47 | */ |
---|
48 | public Date executionDate; |
---|
49 | |
---|
50 | /** |
---|
51 | * Public identifier of this search. Is only used when this query is saved in session |
---|
52 | */ |
---|
53 | public int id; |
---|
54 | |
---|
55 | /** |
---|
56 | * Description of this search. Defaults to 'Search <id>' |
---|
57 | */ |
---|
58 | public String description; |
---|
59 | |
---|
60 | /** |
---|
61 | * URL to view the results of this search |
---|
62 | */ |
---|
63 | public String url; |
---|
64 | |
---|
65 | /** |
---|
66 | * Human readable entity name of the entities that can be found using this search |
---|
67 | */ |
---|
68 | public String entity; |
---|
69 | |
---|
70 | /** |
---|
71 | * Mode to search: OR or AND. |
---|
72 | * @see SearchMode |
---|
73 | */ |
---|
74 | public SearchMode searchMode = SearchMode.and |
---|
75 | |
---|
76 | protected List criteria; |
---|
77 | protected List results; |
---|
78 | protected Map resultFields = [:]; |
---|
79 | |
---|
80 | /** |
---|
81 | * Constructor of this search object. Sets the user field to the |
---|
82 | * currently logged in user |
---|
83 | * @see #user |
---|
84 | */ |
---|
85 | public Search() { |
---|
86 | def ctx = ApplicationHolder.getApplication().getMainContext(); |
---|
87 | def authenticationService = ctx.getBean("authenticationService"); |
---|
88 | def sessionUser = authenticationService?.getLoggedInUser(); |
---|
89 | |
---|
90 | if( sessionUser ) |
---|
91 | this.user = sessionUser; |
---|
92 | else |
---|
93 | this.user = null |
---|
94 | } |
---|
95 | |
---|
96 | /** |
---|
97 | * Returns the number of results found by this search |
---|
98 | * @return |
---|
99 | */ |
---|
100 | public int getNumResults() { |
---|
101 | if( results ) |
---|
102 | return results.size(); |
---|
103 | |
---|
104 | return 0; |
---|
105 | } |
---|
106 | |
---|
107 | /** |
---|
108 | * Executes a search based on the given criteria. Should be filled in by |
---|
109 | * subclasses searching for a specific entity |
---|
110 | * |
---|
111 | * @param c List with criteria to search on |
---|
112 | */ |
---|
113 | public void execute( List c ) { |
---|
114 | setCriteria( c ); |
---|
115 | execute(); |
---|
116 | } |
---|
117 | |
---|
118 | /** |
---|
119 | * Executes a search based on the given criteria. |
---|
120 | */ |
---|
121 | public void execute() { |
---|
122 | this.executionDate = new Date(); |
---|
123 | |
---|
124 | // Execute the search |
---|
125 | executeSearch(); |
---|
126 | |
---|
127 | // Save the value of this results for later use |
---|
128 | saveResultFields(); |
---|
129 | } |
---|
130 | |
---|
131 | /** |
---|
132 | * Executes a query |
---|
133 | */ |
---|
134 | protected void executeSearch() { |
---|
135 | // Create HQL query for criteria for the entity being sought |
---|
136 | def selectClause = "" |
---|
137 | def fullHQL = createHQLForEntity( this.entity ); |
---|
138 | |
---|
139 | // Create SQL for other entities, by executing a subquery first, and |
---|
140 | // afterwards selecting the study based on the entities found |
---|
141 | def resultsFound |
---|
142 | |
---|
143 | def entityNames = [ "Study", "Subject", "Sample", "Assay", "Event", "SamplingEvent" ]; |
---|
144 | for( entityToSearch in entityNames ) { |
---|
145 | // Add conditions for all criteria for the given entity. However, |
---|
146 | // the conditions for the 'main' entity (the entity being sought) are already added |
---|
147 | if( entity != entityToSearch ) { |
---|
148 | resultsFound = addEntityConditions( |
---|
149 | entityToSearch, // Name of the entity to search in |
---|
150 | TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ), // Class of the entity to search in |
---|
151 | elementName( entityToSearch ), // HQL name of the collection to search in |
---|
152 | entityToSearch[0].toLowerCase() + entityToSearch[1..-1], // Alias for the entity to search in |
---|
153 | fullHQL // Current HQL statement |
---|
154 | ) |
---|
155 | |
---|
156 | // If no results are found, and we are searching 'inclusive', there will be no |
---|
157 | // results whatsoever. So we can quit this method now. |
---|
158 | if( !resultsFound && searchMode == SearchMode.and ) { |
---|
159 | return |
---|
160 | } |
---|
161 | } |
---|
162 | } |
---|
163 | |
---|
164 | // Search in all entities |
---|
165 | resultsFound = addWildcardConditions( fullHQL, entityNames ) |
---|
166 | if( !resultsFound && searchMode == SearchMode.and ) { |
---|
167 | return |
---|
168 | } |
---|
169 | |
---|
170 | // Generate where clause |
---|
171 | def whereClause = ""; |
---|
172 | if( fullHQL.where ) { |
---|
173 | whereClause += " ( " + fullHQL.where.join( " " + searchMode.toString() + " " ) + " ) " |
---|
174 | whereClause += " AND "; |
---|
175 | } |
---|
176 | |
---|
177 | // Add a filter such that only readable studies are returned |
---|
178 | def studyName = elementName( "Study" ); |
---|
179 | if( this.user == null ) { |
---|
180 | // Anonymous readers are only given access when published and public |
---|
181 | whereClause += " ( " + studyName + ".publicstudy = true AND " + studyName + ".published = true )" |
---|
182 | } else if( this.user.hasAdminRights() ) { |
---|
183 | // Administrators are allowed to read every study |
---|
184 | whereClause += " (1 = 1)" |
---|
185 | } else { |
---|
186 | // Owners and writers are allowed to read this study |
---|
187 | // Readers are allowed to read this study when it is published |
---|
188 | whereClause += " ( " + studyName + ".owner = :sessionUser OR :sessionUser member of " + studyName + ".writers OR ( :sessionUser member of " + studyName + ".readers AND " + studyName + ".published = true ) )" |
---|
189 | fullHQL.parameters[ "sessionUser" ] = this.user |
---|
190 | } |
---|
191 | |
---|
192 | // Combine all parts to generate a full HQL query |
---|
193 | def hqlQuery = selectClause + " " + fullHQL.from + ( whereClause ? " WHERE " + whereClause : "" ); |
---|
194 | |
---|
195 | // Find all objects |
---|
196 | def entities = entityClass().findAll( hqlQuery, fullHQL.parameters ); |
---|
197 | |
---|
198 | // Find criteria that match one or more 'complex' fields |
---|
199 | // These criteria must be checked extra, since they are not correctly handled |
---|
200 | // by the HQL criteria. See also Criterion.manyToManyWhereCondition and |
---|
201 | // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 |
---|
202 | entities = filterForComplexCriteria( entities, getEntityCriteria( this.entity ) ); |
---|
203 | |
---|
204 | // Filter on module criteria. If the search is 'and', only the entities found until now |
---|
205 | // should be queried in the module. Otherwise, all entities are sent, in order to retrieve |
---|
206 | // data (to show on screen) for all entities |
---|
207 | if( hasModuleCriteria() ) { |
---|
208 | if( searchMode == SearchMode.and ) { |
---|
209 | entities = filterOnModuleCriteria( entities ); |
---|
210 | } else { |
---|
211 | entities = filterOnModuleCriteria( entityClass().list().findAll { this.isAccessible( it ) } ) |
---|
212 | } |
---|
213 | } |
---|
214 | |
---|
215 | // Determine which studies can be read |
---|
216 | results = entities; |
---|
217 | |
---|
218 | } |
---|
219 | |
---|
220 | /************************************************************************ |
---|
221 | * |
---|
222 | * These methods are used in querying and can be overridden by subclasses |
---|
223 | * in order to provide custom searching |
---|
224 | * |
---|
225 | ************************************************************************/ |
---|
226 | |
---|
227 | /** |
---|
228 | * Returns a closure for the given entitytype that determines the value for a criterion |
---|
229 | * on the given object. The closure receives two parameters: the object and a criterion. |
---|
230 | * |
---|
231 | * For example: when searching for studies, the object given to the closure is a Study. |
---|
232 | * Also, when searching for samples, the object given is a Sample. When you have the criterion |
---|
233 | * |
---|
234 | * sample.name equals 'sample 1' |
---|
235 | * |
---|
236 | * and searching for samples, it is easy to determine the value of the object for this criterion: |
---|
237 | * |
---|
238 | * object.getFieldValue( "name" ) |
---|
239 | * |
---|
240 | * |
---|
241 | * However, when searching for samples with the criterion |
---|
242 | * |
---|
243 | * study.title contains 'nbic' |
---|
244 | * |
---|
245 | * this determination is more complex: |
---|
246 | * |
---|
247 | * object.parent.getFieldValue( "title" ) |
---|
248 | * |
---|
249 | * |
---|
250 | * The other way around, when searching for studies with |
---|
251 | * |
---|
252 | * sample.name equals 'sample 1' |
---|
253 | * |
---|
254 | * the value of the 'sample.name' property is a list: |
---|
255 | * |
---|
256 | * object.samples*.getFieldValue( "name" ) |
---|
257 | * |
---|
258 | * The other search methods will handle the list and see whether any of the values |
---|
259 | * matches the criterion. |
---|
260 | * |
---|
261 | * NB. The Criterion object has a convenience method to retrieve the field value on a |
---|
262 | * specific (TemplateEntity) object: getFieldValue. This method also handles |
---|
263 | * non-existing fields and casts the value to the correct type. |
---|
264 | * |
---|
265 | * This method should be overridden by all searches |
---|
266 | * |
---|
267 | * @see Criterion.getFieldValue() |
---|
268 | * |
---|
269 | * @return Closure having 2 parameters: object and criterion |
---|
270 | */ |
---|
271 | protected Closure valueCallback( String entity ) { |
---|
272 | switch( entity ) { |
---|
273 | case "Study": |
---|
274 | case "Subject": |
---|
275 | case "Sample": |
---|
276 | case "Event": |
---|
277 | case "SamplingEvent": |
---|
278 | case "Assay": |
---|
279 | return { object, criterion -> return criterion.getFieldValue( object ); } |
---|
280 | default: |
---|
281 | return null; |
---|
282 | } |
---|
283 | } |
---|
284 | |
---|
285 | /** |
---|
286 | * Returns the HQL name for the element or collections to be searched in, for the given entity name |
---|
287 | * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50. |
---|
288 | * But when searching for Sample results, the system must search in sample.parentSubject for age > 50 |
---|
289 | * |
---|
290 | * This method should be overridden in child classes |
---|
291 | * |
---|
292 | * @param entity Name of the entity of the criterion |
---|
293 | * @return HQL name for this element or collection of elements |
---|
294 | */ |
---|
295 | protected String elementName( String entity ) { |
---|
296 | switch( entity ) { |
---|
297 | case "Study": |
---|
298 | case "Subject": |
---|
299 | case "Sample": |
---|
300 | case "Event": |
---|
301 | case "SamplingEvent": |
---|
302 | case "Assay": |
---|
303 | return entity[ 0 ].toLowerCase() + entity[ 1 .. -1 ] |
---|
304 | default: return null; |
---|
305 | } |
---|
306 | } |
---|
307 | |
---|
308 | /** |
---|
309 | * Returns the a where clause for the given entity name |
---|
310 | * For example: when searching for Subject.age > 50 with Study results, the system must search |
---|
311 | * |
---|
312 | * WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...) |
---|
313 | * |
---|
314 | * The returned string is fed to sprintf with 3 string parameters: |
---|
315 | * from (in this case 'study.subjects' |
---|
316 | * alias (in this case 'subject' |
---|
317 | * paramName (in this case '...') |
---|
318 | * |
---|
319 | * This method can be overridden in child classes to enable specific behaviour |
---|
320 | * |
---|
321 | * @param entity Name of the entity of the criterion |
---|
322 | * @return HQL where clause for this element or collection of elements |
---|
323 | */ |
---|
324 | protected String entityClause( String entity ) { |
---|
325 | return ' EXISTS( FROM %1$s %2$s WHERE %2$s IN (:%3$s) )' |
---|
326 | } |
---|
327 | |
---|
328 | /** |
---|
329 | * Returns true iff the given entity is accessible by the user currently logged in |
---|
330 | * |
---|
331 | * This method should be overridden in child classes, since the check is different for every type of search |
---|
332 | * |
---|
333 | * @param entity Entity to determine accessibility for. The entity is of the type 'this.entity' |
---|
334 | * @return True iff the user is allowed to access this entity |
---|
335 | */ |
---|
336 | protected boolean isAccessible( def entity ) { |
---|
337 | return false |
---|
338 | } |
---|
339 | |
---|
340 | /**************************************************** |
---|
341 | * |
---|
342 | * Helper methods for generating HQL statements |
---|
343 | * |
---|
344 | ****************************************************/ |
---|
345 | |
---|
346 | /** |
---|
347 | * Add all conditions for criteria for a specific entity |
---|
348 | * |
---|
349 | * @param entityName Name of the entity to search in |
---|
350 | * @param entityClass Class of the entity to search |
---|
351 | * @param from Name of the HQL collection to search in (e.g. study.subjects) |
---|
352 | * @param alias Alias of the HQL collection objects (e.g. 'subject') |
---|
353 | * @param fullHQL Original HQL map to be extended (fields 'from', 'where' and 'parameters') |
---|
354 | * @param determineParentId Closure to determine the id of the final entity to search, based on these objects |
---|
355 | * @param entityCriteria (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found |
---|
356 | * @return True if one ore more entities are found, false otherwise |
---|
357 | */ |
---|
358 | protected boolean addEntityConditions( String entityName, def entityClass, String from, String alias, def fullHQL, def entityCriteria = null ) { |
---|
359 | if( entityCriteria == null ) |
---|
360 | entityCriteria = getEntityCriteria( entityName ) |
---|
361 | |
---|
362 | // Create HQL for these criteria |
---|
363 | def entityHQL = createHQLForEntity( entityName, entityCriteria ); |
---|
364 | |
---|
365 | // If any clauses are generated for these criteria, find entities that match these criteria |
---|
366 | def whereClauses = entityHQL.where?.findAll { it && it?.trim() != "" } |
---|
367 | if( whereClauses ) { |
---|
368 | // First find all entities that match these criteria |
---|
369 | def hqlQuery = entityHQL.from + " WHERE " + whereClauses.join( searchMode == SearchMode.and ? " AND " : " OR " ); |
---|
370 | def entities = entityClass.findAll( hqlQuery, entityHQL.parameters ) |
---|
371 | |
---|
372 | // If there are entities matching these criteria, put a where clause in the full HQL query |
---|
373 | if( entities ) { |
---|
374 | // Find criteria that match one or more 'complex' fields |
---|
375 | // These criteria must be checked extra, since they are not correctly handled |
---|
376 | // by the HQL criteria. See also Criterion.manyToManyWhereCondition and |
---|
377 | // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 |
---|
378 | entities = filterForComplexCriteria( entities, entityCriteria ); |
---|
379 | |
---|
380 | if( entities ) { |
---|
381 | def paramName = from.replaceAll( /\W/, '' ); |
---|
382 | fullHQL.where << sprintf( entityClause( entityName ), from, alias, paramName ); |
---|
383 | fullHQL.parameters[ paramName ] = entities |
---|
384 | return true; |
---|
385 | } |
---|
386 | } |
---|
387 | |
---|
388 | // No results are found. |
---|
389 | results = []; |
---|
390 | return false |
---|
391 | } |
---|
392 | |
---|
393 | return true; |
---|
394 | } |
---|
395 | |
---|
396 | /** |
---|
397 | * Add all conditions for a wildcard search (all fields in a given entity) |
---|
398 | * @param fullHQL Original HQL map to be extended (fields 'from', 'where' and 'parameters') |
---|
399 | * @return True if the addition worked |
---|
400 | */ |
---|
401 | protected boolean addWildcardConditions( def fullHQL, def entities) { |
---|
402 | // Append study criteria |
---|
403 | def entityCriteria = getEntityCriteria( "*" ); |
---|
404 | |
---|
405 | // If no wildcard criteria are found, return immediately |
---|
406 | if( !entityCriteria ) |
---|
407 | return true |
---|
408 | |
---|
409 | // Wildcards should be checked within each entity |
---|
410 | def wildcardHQL = createHQLForEntity( this.entity, entityCriteria, false ); |
---|
411 | |
---|
412 | // Create SQL for other entities, by executing a subquery first, and |
---|
413 | // afterwards selecting the study based on the entities found |
---|
414 | entities.each { entityToSearch -> |
---|
415 | // Add conditions for all criteria for the given entity. However, |
---|
416 | // the conditions for the 'main' entity (the entity being sought) are already added |
---|
417 | if( entity != entityToSearch ) { |
---|
418 | addEntityConditions( |
---|
419 | entityToSearch, // Name of the entity to search in |
---|
420 | TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ), // Class of the entity to search in |
---|
421 | elementName( entityToSearch ), // HQL name of the collection to search in |
---|
422 | entityToSearch[0].toLowerCase() + entityToSearch[1..-1], // Alias for the entity to search in |
---|
423 | wildcardHQL, // Current HQL statement |
---|
424 | entityCriteria // Only create HQL for these criteria |
---|
425 | ) |
---|
426 | } |
---|
427 | } |
---|
428 | |
---|
429 | // Add these clauses to the full HQL statement |
---|
430 | def whereClauses = wildcardHQL.where.findAll { it }; |
---|
431 | |
---|
432 | if( whereClauses ) { |
---|
433 | fullHQL.from += wildcardHQL.from |
---|
434 | fullHQL.where << whereClauses.findAll { it }.join( " OR " ) |
---|
435 | |
---|
436 | wildcardHQL[ "parameters" ].each { |
---|
437 | fullHQL.parameters[ it.key ] = it.value |
---|
438 | } |
---|
439 | } |
---|
440 | |
---|
441 | return true; |
---|
442 | } |
---|
443 | |
---|
444 | /** |
---|
445 | * Create HQL statement for the given criteria and a specific entity |
---|
446 | * @param entityName Name of the entity |
---|
447 | * @param entityCriteria (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found |
---|
448 | * @param includeFrom (optional) If set to true, the 'FROM entity' is prepended to the from clause. Defaults to true |
---|
449 | * @return |
---|
450 | */ |
---|
451 | def createHQLForEntity( String entityName, def entityCriteria = null, includeFrom = true ) { |
---|
452 | def fromClause = includeFrom ? "FROM " + entityName + " " + entityName.toLowerCase() : "" |
---|
453 | def whereClause = [] |
---|
454 | def parameters = [:] |
---|
455 | def criterionNum = 0; |
---|
456 | |
---|
457 | // Append study criteria |
---|
458 | if( entityCriteria == null ) |
---|
459 | entityCriteria = getEntityCriteria( entityName ); |
---|
460 | |
---|
461 | entityCriteria.each { |
---|
462 | def criteriaHQL = it.toHQL( "criterion" +entityName + criterionNum++, entityName.toLowerCase() ); |
---|
463 | |
---|
464 | if( criteriaHQL[ "join" ] ) |
---|
465 | fromClause += " " + criteriaHQL[ "join" ] |
---|
466 | |
---|
467 | if( criteriaHQL[ "where" ] ) |
---|
468 | whereClause << criteriaHQL[ "where" ] |
---|
469 | |
---|
470 | criteriaHQL[ "parameters" ].each { |
---|
471 | parameters[ it.key ] = it.value |
---|
472 | } |
---|
473 | } |
---|
474 | |
---|
475 | return [ "from": fromClause, "where": whereClause, "parameters": parameters ] |
---|
476 | } |
---|
477 | |
---|
478 | /***************************************************** |
---|
479 | * |
---|
480 | * The other methods are helper functions for the execution of queries in subclasses |
---|
481 | * |
---|
482 | *****************************************************/ |
---|
483 | |
---|
484 | /** |
---|
485 | * Returns a list of criteria targeted on the given entity |
---|
486 | * @param entity Entity to search criteria for |
---|
487 | * @return List of criteria |
---|
488 | */ |
---|
489 | protected List getEntityCriteria( String entity ) { |
---|
490 | return criteria?.findAll { it.entity == entity } |
---|
491 | } |
---|
492 | |
---|
493 | |
---|
494 | /** |
---|
495 | * Prepares a value from a template entity for comparison, by giving it a correct type |
---|
496 | * |
---|
497 | * @param value Value of the field |
---|
498 | * @param type TemplateFieldType Type of the specific field |
---|
499 | * @return The value of the field in the correct entity |
---|
500 | */ |
---|
501 | public static def prepare( def value, TemplateFieldType type ) { |
---|
502 | if( value == null ) |
---|
503 | return value |
---|
504 | |
---|
505 | switch (type) { |
---|
506 | case TemplateFieldType.DATE: |
---|
507 | try { |
---|
508 | return new SimpleDateFormat( "yyyy-MM-dd" ).parse( value.toString() ) |
---|
509 | } catch( Exception e ) { |
---|
510 | return value.toString(); |
---|
511 | } |
---|
512 | case TemplateFieldType.RELTIME: |
---|
513 | try { |
---|
514 | if( value instanceof Number ) { |
---|
515 | return new RelTime( value ); |
---|
516 | } else if( value.toString().isNumber() ) { |
---|
517 | return new RelTime( Long.parseLong( value.toString() ) ) |
---|
518 | } else { |
---|
519 | return new RelTime( value ); |
---|
520 | } |
---|
521 | } catch( Exception e ) { |
---|
522 | try { |
---|
523 | return Long.parseLong( value ) |
---|
524 | } catch( Exception e2 ) { |
---|
525 | return value.toString(); |
---|
526 | } |
---|
527 | } |
---|
528 | case TemplateFieldType.DOUBLE: |
---|
529 | try { |
---|
530 | return Double.valueOf( value ) |
---|
531 | } catch( Exception e ) { |
---|
532 | return value.toString(); |
---|
533 | } |
---|
534 | case TemplateFieldType.BOOLEAN: |
---|
535 | try { |
---|
536 | return Boolean.valueOf( value ) |
---|
537 | } catch( Exception e ) { |
---|
538 | return value.toString(); |
---|
539 | } |
---|
540 | case TemplateFieldType.LONG: |
---|
541 | try { |
---|
542 | return Long.valueOf( value ) |
---|
543 | } catch( Exception e ) { |
---|
544 | return value.toString(); |
---|
545 | } |
---|
546 | case TemplateFieldType.STRING: |
---|
547 | case TemplateFieldType.TEXT: |
---|
548 | case TemplateFieldType.STRINGLIST: |
---|
549 | case TemplateFieldType.TEMPLATE: |
---|
550 | case TemplateFieldType.MODULE: |
---|
551 | case TemplateFieldType.FILE: |
---|
552 | case TemplateFieldType.ONTOLOGYTERM: |
---|
553 | default: |
---|
554 | return value.toString(); |
---|
555 | } |
---|
556 | |
---|
557 | } |
---|
558 | |
---|
559 | /***************************************************** |
---|
560 | * |
---|
561 | * Methods for filtering lists based on specific (GSCF) criteria |
---|
562 | * |
---|
563 | *****************************************************/ |
---|
564 | |
---|
565 | |
---|
566 | /** |
---|
567 | * Filters a list with entities, based on the given criteria and a closure to check whether a criterion is matched |
---|
568 | * |
---|
569 | * @param entities Original list with entities to check for these criteria |
---|
570 | * @param criteria List with criteria to match on |
---|
571 | * @param check Closure to see whether a specific entity matches a criterion. Gets two arguments: |
---|
572 | * element The element to check |
---|
573 | * criterion The criterion to check on. |
---|
574 | * Returns true if the criterion holds, false otherwise |
---|
575 | * @return The filtered list of entities |
---|
576 | */ |
---|
577 | protected List filterEntityList( List entities, List<Criterion> criteria, Closure check ) { |
---|
578 | if( !entities || !criteria || criteria.size() == 0 ) { |
---|
579 | if( searchMode == SearchMode.and ) |
---|
580 | return entities; |
---|
581 | else if( searchMode == SearchMode.or ) |
---|
582 | return [] |
---|
583 | } |
---|
584 | |
---|
585 | return entities.findAll { entity -> |
---|
586 | if( searchMode == SearchMode.and ) { |
---|
587 | for( criterion in criteria ) { |
---|
588 | if( !check( entity, criterion ) ) { |
---|
589 | return false; |
---|
590 | } |
---|
591 | } |
---|
592 | return true; |
---|
593 | } else if( searchMode == SearchMode.or ) { |
---|
594 | for( criterion in criteria ) { |
---|
595 | if( check( entity, criterion ) ) { |
---|
596 | return true; |
---|
597 | } |
---|
598 | } |
---|
599 | return false; |
---|
600 | } |
---|
601 | } |
---|
602 | } |
---|
603 | |
---|
604 | /** |
---|
605 | * Filters an entity list manually on complex criteria found in the criteria list. |
---|
606 | * This method is needed because hibernate contains a bug in the HQL INDEX() function. |
---|
607 | * See also Criterion.manyToManyWhereCondition and |
---|
608 | * http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 |
---|
609 | * |
---|
610 | * @param entities List of entities |
---|
611 | * @param entityCriteria List of criteria that apply to the type of entities given (e.g. Subject criteria for Subjects) |
---|
612 | * @return Filtered entity list |
---|
613 | */ |
---|
614 | protected filterForComplexCriteria( def entities, def entityCriteria ) { |
---|
615 | def complexCriteria = entityCriteria.findAll { it.isComplexCriterion() } |
---|
616 | |
---|
617 | if( complexCriteria ) { |
---|
618 | def checkCallback = { entity, criterion -> |
---|
619 | def value = criterion.getFieldValue( entity ) |
---|
620 | |
---|
621 | if( value == null ) { |
---|
622 | return false |
---|
623 | } |
---|
624 | |
---|
625 | if( value instanceof Collection ) { |
---|
626 | return value.any { criterion.match( it ) } |
---|
627 | } else { |
---|
628 | return criterion.match( value ); |
---|
629 | } |
---|
630 | } |
---|
631 | |
---|
632 | entities = filterEntityList( entities, complexCriteria, checkCallback ); |
---|
633 | } |
---|
634 | |
---|
635 | return entities; |
---|
636 | } |
---|
637 | |
---|
638 | /******************************************************************** |
---|
639 | * |
---|
640 | * Methods for filtering object lists on module criteria |
---|
641 | * |
---|
642 | ********************************************************************/ |
---|
643 | |
---|
644 | protected boolean hasModuleCriteria() { |
---|
645 | |
---|
646 | return AssayModule.list().any { module -> |
---|
647 | // Remove 'module' from module name |
---|
648 | def moduleName = module.name.replace( 'module', '' ).trim() |
---|
649 | def moduleCriteria = getEntityCriteria( moduleName ); |
---|
650 | return moduleCriteria?.size() > 0 |
---|
651 | } |
---|
652 | } |
---|
653 | |
---|
654 | /** |
---|
655 | * Filters the given list of entities on the module criteria |
---|
656 | * @param entities Original list of entities. Entities should expose a giveUUID() method to give the token. |
---|
657 | * @return List with all entities that match the module criteria |
---|
658 | */ |
---|
659 | protected List filterOnModuleCriteria( List entities ) { |
---|
660 | // An empty list can't be filtered more than is has been now |
---|
661 | if( !entities || entities.size() == 0 ) |
---|
662 | return []; |
---|
663 | |
---|
664 | // Determine the moduleCommunicationService. Because this object |
---|
665 | // is mocked in the tests, it can't be converted to a ApplicationContext object |
---|
666 | def ctx = ApplicationHolder.getApplication().getMainContext(); |
---|
667 | def moduleCommunicationService = ctx.getBean("moduleCommunicationService"); |
---|
668 | |
---|
669 | switch( searchMode ) { |
---|
670 | case SearchMode.and: |
---|
671 | // Loop through all modules and check whether criteria have been given |
---|
672 | // for that module |
---|
673 | AssayModule.list().each { module -> |
---|
674 | // Remove 'module' from module name |
---|
675 | def moduleName = module.name.replace( 'module', '' ).trim() |
---|
676 | def moduleCriteria = getEntityCriteria( moduleName ); |
---|
677 | |
---|
678 | if( moduleCriteria && moduleCriteria.size() > 0 ) { |
---|
679 | def callUrl = moduleCriteriaUrl( module ); |
---|
680 | def callArgs = moduleCriteriaArguments( module, entities, moduleCriteria ); |
---|
681 | |
---|
682 | try { |
---|
683 | def json = moduleCommunicationService.callModuleMethod( module.url, callUrl, callArgs, "POST" ); |
---|
684 | Closure checkClosure = moduleCriterionClosure( json ); |
---|
685 | entities = filterEntityList( entities, moduleCriteria, checkClosure ); |
---|
686 | } catch( Exception e ) { |
---|
687 | //log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() ) |
---|
688 | e.printStackTrace() |
---|
689 | throw e |
---|
690 | } |
---|
691 | } |
---|
692 | } |
---|
693 | |
---|
694 | return entities; |
---|
695 | case SearchMode.or: |
---|
696 | def resultingEntities = [] |
---|
697 | |
---|
698 | // Loop through all modules and check whether criteria have been given |
---|
699 | // for that module |
---|
700 | AssayModule.list().each { module -> |
---|
701 | // Remove 'module' from module name |
---|
702 | def moduleName = module.name.replace( 'module', '' ).trim() |
---|
703 | def moduleCriteria = getEntityCriteria( moduleName ); |
---|
704 | |
---|
705 | if( moduleCriteria && moduleCriteria.size() > 0 ) { |
---|
706 | def callUrl = moduleCriteriaUrl( module ); |
---|
707 | def callArgs = moduleCriteriaArguments( module, entities, moduleCriteria ); |
---|
708 | |
---|
709 | try { |
---|
710 | def json = moduleCommunicationService.callModuleMethod( module.url, callUrl, callArgs, "POST" ); |
---|
711 | Closure checkClosure = moduleCriterionClosure( json ); |
---|
712 | |
---|
713 | resultingEntities += filterEntityList( entities, moduleCriteria, checkClosure ); |
---|
714 | resultingEntities = resultingEntities.unique(); |
---|
715 | |
---|
716 | } catch( Exception e ) { |
---|
717 | //log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() ) |
---|
718 | e.printStackTrace() |
---|
719 | throw e |
---|
720 | } |
---|
721 | } |
---|
722 | } |
---|
723 | |
---|
724 | return resultingEntities; |
---|
725 | default: |
---|
726 | return []; |
---|
727 | } |
---|
728 | } |
---|
729 | |
---|
730 | /** |
---|
731 | * Returns a closure for determining the value of a module field |
---|
732 | * @param json |
---|
733 | * @return |
---|
734 | */ |
---|
735 | protected Closure moduleCriterionClosure( def json ) { |
---|
736 | return { entity, criterion -> |
---|
737 | // Find the value of the field in this sample. That value is still in the |
---|
738 | // JSON object |
---|
739 | def token = entity.giveUUID() |
---|
740 | def value |
---|
741 | |
---|
742 | if( criterion.field == '*' ) { |
---|
743 | // Collect the values from all fields |
---|
744 | value = []; |
---|
745 | json[ token ].each { field -> |
---|
746 | if( field.value instanceof Collection ) { |
---|
747 | field.value.each { value << it } |
---|
748 | } else { |
---|
749 | value << field.value; |
---|
750 | } |
---|
751 | } |
---|
752 | } else { |
---|
753 | if( !json[ token ] || json[ token ][ criterion.field ] == null ) |
---|
754 | return false; |
---|
755 | |
---|
756 | // Check whether a list or string is given |
---|
757 | value = json[ token ][ criterion.field ]; |
---|
758 | |
---|
759 | // Save the value of this entity for later use |
---|
760 | saveResultField( entity.id, criterion.entity + " " + criterion.field, value ) |
---|
761 | |
---|
762 | if( !( value instanceof Collection ) ) { |
---|
763 | value = [ value ]; |
---|
764 | } |
---|
765 | } |
---|
766 | |
---|
767 | // Convert numbers to a long or double in order to process them correctly |
---|
768 | def values = value.collect { val -> |
---|
769 | val = val.toString(); |
---|
770 | if( val.isLong() ) { |
---|
771 | val = Long.parseLong( val ); |
---|
772 | } else if( val.isDouble() ) { |
---|
773 | val = Double.parseDouble( val ); |
---|
774 | } |
---|
775 | return val; |
---|
776 | } |
---|
777 | |
---|
778 | // Loop through all values and match any |
---|
779 | for( val in values ) { |
---|
780 | if( criterion.match( val ) ) |
---|
781 | return true; |
---|
782 | } |
---|
783 | |
---|
784 | return false; |
---|
785 | } |
---|
786 | } |
---|
787 | |
---|
788 | protected String moduleCriteriaUrl( module ) { |
---|
789 | def callUrl = module.url + '/rest/getQueryableFieldData' |
---|
790 | return callUrl; |
---|
791 | } |
---|
792 | |
---|
793 | protected String moduleCriteriaArguments( module, entities, moduleCriteria ) { |
---|
794 | // Retrieve the data from the module |
---|
795 | def tokens = entities.collect { it.giveUUID() }.unique(); |
---|
796 | def fields = moduleCriteria.collect { it.field }.unique(); |
---|
797 | |
---|
798 | def callUrl = 'entity=' + this.entity |
---|
799 | tokens.sort().each { callUrl += "&tokens=" + it.encodeAsURL() } |
---|
800 | |
---|
801 | // If all fields are searched, all fields should be retrieved |
---|
802 | if( fields.contains( '*' ) ) { |
---|
803 | |
---|
804 | } else { |
---|
805 | fields.sort().each { callUrl += "&fields=" + it.encodeAsURL() } |
---|
806 | } |
---|
807 | |
---|
808 | return callUrl; |
---|
809 | } |
---|
810 | |
---|
811 | /********************************************************************* |
---|
812 | * |
---|
813 | * These methods are used for saving information about the search results and showing the information later on. |
---|
814 | * |
---|
815 | *********************************************************************/ |
---|
816 | |
---|
817 | /** |
---|
818 | * Saves data about template entities to use later on. This data is copied to a special |
---|
819 | * structure to make it compatible with data fetched from other modules. |
---|
820 | * @see #saveResultField() |
---|
821 | */ |
---|
822 | protected void saveResultFields() { |
---|
823 | if( !results || !criteria ) |
---|
824 | return |
---|
825 | |
---|
826 | criteria.each { criterion -> |
---|
827 | if( criterion.field && criterion.field != '*' ) { |
---|
828 | def valueCallback = valueCallback( criterion.entity ); |
---|
829 | |
---|
830 | if( valueCallback != null ) { |
---|
831 | def name = criterion.entity + ' ' + criterion.field |
---|
832 | |
---|
833 | results.each { result -> |
---|
834 | saveResultField( result.id, name, valueCallback( result, criterion ) ); |
---|
835 | } |
---|
836 | } |
---|
837 | } |
---|
838 | } |
---|
839 | } |
---|
840 | |
---|
841 | /** |
---|
842 | * Saves data about template entities to use later on. This data is copied to a special |
---|
843 | * structure to make it compatible with data fetched from other modules. |
---|
844 | * @param entities List of template entities to find data in |
---|
845 | * @param criteria Criteria to search for |
---|
846 | * @param valueCallback Callback to retrieve a specific field from the entity |
---|
847 | * @see #saveResultField() |
---|
848 | */ |
---|
849 | protected void saveResultFields( entities, criteria, valueCallback ) { |
---|
850 | for( criterion in criteria ) { |
---|
851 | for( entity in entities ) { |
---|
852 | if( criterion.field && criterion.field != '*' ) |
---|
853 | saveResultField( entity.id, criterion.entity + ' ' + criterion.field, valueCallback( entity, criterion ) ) |
---|
854 | } |
---|
855 | } |
---|
856 | } |
---|
857 | |
---|
858 | |
---|
859 | /** |
---|
860 | * Saves a specific field of an object to use later on. Especially useful when looking up data from other modules. |
---|
861 | * @param id ID of the object |
---|
862 | * @param fieldName Field name that has been searched |
---|
863 | * @param value Value of the field |
---|
864 | */ |
---|
865 | protected void saveResultField( id, fieldName, value ) { |
---|
866 | if( resultFields[ id ] == null ) |
---|
867 | resultFields[ id ] = [:] |
---|
868 | |
---|
869 | // Handle special cases |
---|
870 | if( value == null ) |
---|
871 | value = ""; |
---|
872 | |
---|
873 | if( fieldName == "*" ) |
---|
874 | return; |
---|
875 | |
---|
876 | if( value instanceof Collection ) { |
---|
877 | value = value.findAll { it != null } |
---|
878 | } |
---|
879 | |
---|
880 | resultFields[ id ][ fieldName ] = value; |
---|
881 | } |
---|
882 | |
---|
883 | /** |
---|
884 | * Removes all data from the result field map |
---|
885 | */ |
---|
886 | protected void clearResultFields() { |
---|
887 | resultFields = [:] |
---|
888 | } |
---|
889 | |
---|
890 | /** |
---|
891 | * Returns the saved field data that could be shown on screen. This means, the data is filtered to show only data of the query results. |
---|
892 | * |
---|
893 | * Subclasses could filter out the fields they don't want to show on the result screen (e.g. because they are shown regardless of the |
---|
894 | * query.) |
---|
895 | * @return Map with the entity id as a key, and a field-value map as value |
---|
896 | */ |
---|
897 | public Map getShowableResultFields() { |
---|
898 | def resultIds = getResults()*.id; |
---|
899 | return getResultFields().findAll { |
---|
900 | resultIds.contains( it.key ) |
---|
901 | } |
---|
902 | } |
---|
903 | |
---|
904 | /** |
---|
905 | * Returns the field names that are found in the map with showable result fields |
---|
906 | * |
---|
907 | * @param fields Map with showable result fields |
---|
908 | * @see getShowableResultFields |
---|
909 | * @return |
---|
910 | */ |
---|
911 | public List getShowableResultFieldNames( fields ) { |
---|
912 | return fields.values()*.keySet().flatten().unique(); |
---|
913 | } |
---|
914 | |
---|
915 | |
---|
916 | /************************************************************************ |
---|
917 | * |
---|
918 | * Getters and setters |
---|
919 | * |
---|
920 | ************************************************************************/ |
---|
921 | |
---|
922 | /** |
---|
923 | * Returns a list of Criteria |
---|
924 | */ |
---|
925 | public List getCriteria() { return criteria; } |
---|
926 | |
---|
927 | /** |
---|
928 | * Sets a new list of criteria |
---|
929 | * @param c List with criteria objects |
---|
930 | */ |
---|
931 | public void setCriteria( List c ) { criteria = c; } |
---|
932 | |
---|
933 | /** |
---|
934 | * Adds a criterion to this query |
---|
935 | * @param c Criterion |
---|
936 | */ |
---|
937 | public void addCriterion( Criterion c ) { |
---|
938 | if( criteria ) |
---|
939 | criteria << c; |
---|
940 | else |
---|
941 | criteria = [c]; |
---|
942 | } |
---|
943 | |
---|
944 | /** |
---|
945 | * Retrieves the results found using this query. The result is empty is |
---|
946 | * the query has not been executed yet. |
---|
947 | */ |
---|
948 | public List getResults() { return results; } |
---|
949 | |
---|
950 | /** |
---|
951 | * Returns the results found using this query, filtered by a list of ids. |
---|
952 | * @param selectedIds List with ids of the entities you want to return. |
---|
953 | * @return A list with only those results for which the id is in the selectedIds |
---|
954 | */ |
---|
955 | public List filterResults( List selectedTokens ) { |
---|
956 | if( !selectedTokens || !results ) |
---|
957 | return results |
---|
958 | |
---|
959 | return results.findAll { |
---|
960 | selectedTokens.contains( it.giveUUID() ) |
---|
961 | } |
---|
962 | } |
---|
963 | |
---|
964 | /** |
---|
965 | * Returns a list of fields for the results of this query. The fields returned are those |
---|
966 | * fields that the query searched for. |
---|
967 | */ |
---|
968 | public Map getResultFields() { return resultFields; } |
---|
969 | |
---|
970 | public String toString() { |
---|
971 | if( this.description ) { |
---|
972 | return this.description |
---|
973 | } else if( this.entity ) { |
---|
974 | return this.entity + " search " + this.id; |
---|
975 | } else { |
---|
976 | return "Search " + this.id |
---|
977 | } |
---|
978 | } |
---|
979 | |
---|
980 | public boolean equals( Object o ) { |
---|
981 | if( o == null ) |
---|
982 | return false |
---|
983 | |
---|
984 | if( !( o instanceof Search ) ) |
---|
985 | return false |
---|
986 | |
---|
987 | Search s = (Search) o; |
---|
988 | |
---|
989 | // Determine criteria equality |
---|
990 | def criteriaEqual = false; |
---|
991 | if( !criteria && !s.criteria ) { |
---|
992 | criteriaEqual = true; |
---|
993 | } else if( criteria && s.criteria ) { |
---|
994 | criteriaEqual = criteria.size()== s.criteria.size() && |
---|
995 | s.criteria.containsAll( criteria ) && |
---|
996 | criteria.containsAll( s.criteria ) |
---|
997 | } |
---|
998 | |
---|
999 | return ( searchMode == s.searchMode && |
---|
1000 | entity == s.entity && |
---|
1001 | criteriaEqual |
---|
1002 | ); |
---|
1003 | } |
---|
1004 | |
---|
1005 | /** |
---|
1006 | * Returns the class for the entity being searched |
---|
1007 | * @return |
---|
1008 | */ |
---|
1009 | public Class entityClass() { |
---|
1010 | if( !this.entity ) |
---|
1011 | return null; |
---|
1012 | |
---|
1013 | try { |
---|
1014 | return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity) |
---|
1015 | } catch( Exception e ) { |
---|
1016 | throw new Exception( "Unknown entity for criterion " + this, e ); |
---|
1017 | } |
---|
1018 | } |
---|
1019 | |
---|
1020 | /** |
---|
1021 | * Registers a query that has been performed somewhere else, but used in GSCF (e.g. refined) |
---|
1022 | * |
---|
1023 | * @param description Description of the search |
---|
1024 | * @param url Url to view the search results |
---|
1025 | * @param entity Entity that has been sought |
---|
1026 | * @param results List of |
---|
1027 | * @return |
---|
1028 | */ |
---|
1029 | public static Search register( String description, String url, String entity, def results ) { |
---|
1030 | Search s; |
---|
1031 | |
---|
1032 | // Determine entity |
---|
1033 | switch( entity ) { |
---|
1034 | case "Study": |
---|
1035 | s = new StudySearch(); |
---|
1036 | break; |
---|
1037 | case "Assay": |
---|
1038 | s = new AssaySearch(); |
---|
1039 | break; |
---|
1040 | case "Sample": |
---|
1041 | s = new SampleSearch(); |
---|
1042 | break; |
---|
1043 | default: |
---|
1044 | return null; |
---|
1045 | } |
---|
1046 | |
---|
1047 | // Set properties |
---|
1048 | s.description = description; |
---|
1049 | s.url = url |
---|
1050 | s.results = results |
---|
1051 | |
---|
1052 | return s; |
---|
1053 | } |
---|
1054 | } |
---|