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