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