1 | package dbnp.query |
---|
2 | |
---|
3 | import dbnp.modules.* |
---|
4 | import org.dbnp.gdt.* |
---|
5 | import dbnp.studycapturing.*; |
---|
6 | import grails.converters.JSON |
---|
7 | |
---|
8 | /** |
---|
9 | * Basic web interface for searching within studies |
---|
10 | * |
---|
11 | * @author Robert Horlings (robert@isdat.nl) |
---|
12 | */ |
---|
13 | class AdvancedQueryController { |
---|
14 | def moduleCommunicationService; |
---|
15 | def authenticationService |
---|
16 | |
---|
17 | def entitiesToSearchFor = [ 'Study': 'Studies', 'Sample': 'Samples', 'Assay': 'Assays'] |
---|
18 | |
---|
19 | /** |
---|
20 | * Shows search screen |
---|
21 | */ |
---|
22 | def index = { |
---|
23 | // Check whether criteria have been given before |
---|
24 | def criteria = []; |
---|
25 | if( params.criteria ) { |
---|
26 | criteria = parseCriteria( params.criteria, false ) |
---|
27 | } |
---|
28 | [searchModes: SearchMode.values(), entitiesToSearchFor: entitiesToSearchFor, searchableFields: getSearchableFields(), criteria: criteria, previousSearches: session.queries ] |
---|
29 | } |
---|
30 | |
---|
31 | /** |
---|
32 | * Searches for studies or samples based on the user parameters. |
---|
33 | * |
---|
34 | * @param entity The entity to search for ( 'Study' or 'Sample' ) |
---|
35 | * @param criteria HashMap with the values being hashmaps with field, operator and value. |
---|
36 | * [ 0: [ field: 'Study.name', operator: 'equals', value: 'term' ], 1: [..], .. ] |
---|
37 | */ |
---|
38 | def search = { |
---|
39 | if( !params.criteria ) { |
---|
40 | flash.error = "No criteria given to search for. Please try again."; |
---|
41 | redirect( action: 'index' ) |
---|
42 | } |
---|
43 | |
---|
44 | if( !params.entity || !entitiesToSearchFor*.key.contains( params.entity ) ) { |
---|
45 | flash.error = "No or incorrect entity given to search for. Please try again."; |
---|
46 | redirect( action: 'index', params: [ criteria: parseCriteria( params.criteria ) ] ) |
---|
47 | } |
---|
48 | |
---|
49 | // Create a search object and let it do the searching |
---|
50 | Search search = determineSearch( params.entity ); |
---|
51 | String view = determineView( params.entity ); |
---|
52 | |
---|
53 | // Choose between AND and OR search. Default is given by the Search class itself. |
---|
54 | switch( params.operator?.toString()?.toLowerCase() ) { |
---|
55 | case "or": |
---|
56 | search.searchMode = SearchMode.or; |
---|
57 | break; |
---|
58 | case "and": |
---|
59 | search.searchMode = SearchMode.and; |
---|
60 | break; |
---|
61 | } |
---|
62 | |
---|
63 | search.execute( parseCriteria( params.criteria ) ); |
---|
64 | |
---|
65 | // Save search in session |
---|
66 | def queryId = saveSearch( search ); |
---|
67 | render( view: view, model: [search: search, queryId: queryId, actions: determineActions(search)] ); |
---|
68 | } |
---|
69 | |
---|
70 | /** |
---|
71 | * Removes a specified search from session |
---|
72 | * @param id queryId of the search to discard |
---|
73 | */ |
---|
74 | def discard = { |
---|
75 | def queryIds = params.list( 'id' ); |
---|
76 | queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) } |
---|
77 | |
---|
78 | if( queryIds.size() == 0 ) { |
---|
79 | flash.error = "Incorrect search ID given to discard" |
---|
80 | redirect( action: "index" ); |
---|
81 | return |
---|
82 | } |
---|
83 | |
---|
84 | queryIds.each { queryId -> |
---|
85 | discardSearch( queryId ); |
---|
86 | } |
---|
87 | |
---|
88 | if( queryIds.size() > 1 ) { |
---|
89 | flash.message = "Searches have been discarded" |
---|
90 | } else { |
---|
91 | flash.message = "Search has been discarded" |
---|
92 | } |
---|
93 | redirect( action: "list" ); |
---|
94 | } |
---|
95 | |
---|
96 | /** |
---|
97 | * Shows a specified search from session |
---|
98 | * @param id queryId of the search to show |
---|
99 | */ |
---|
100 | def show = { |
---|
101 | def queryId = params.int( 'id' ); |
---|
102 | |
---|
103 | if( !queryId ) { |
---|
104 | flash.error = "Incorrect search ID given to show" |
---|
105 | redirect( action: "index" ); |
---|
106 | return |
---|
107 | } |
---|
108 | |
---|
109 | // Retrieve the search from session |
---|
110 | Search s = retrieveSearch( queryId ); |
---|
111 | if( !s ) { |
---|
112 | flash.message = "Specified search could not be found" |
---|
113 | redirect( action: "index" ); |
---|
114 | return; |
---|
115 | } |
---|
116 | |
---|
117 | // Attach all objects to the current hibernate thread, because the |
---|
118 | // object might be attached to an old thread, since the results are |
---|
119 | // saved in session |
---|
120 | s.getResults().each { |
---|
121 | it.attach(); |
---|
122 | } |
---|
123 | |
---|
124 | // Determine which view to show |
---|
125 | def view = determineView( s.entity ); |
---|
126 | render( view: view, model: [search: s, queryId: queryId, actions: determineActions(s)] ); |
---|
127 | } |
---|
128 | |
---|
129 | /** |
---|
130 | * Shows a list of searches that have been saved in session |
---|
131 | * @param id queryId of the search to show |
---|
132 | */ |
---|
133 | def list = { |
---|
134 | def searches = listSearches(); |
---|
135 | |
---|
136 | if( !searches || searches.size() == 0 ) { |
---|
137 | flash.message = "No previous searches found"; |
---|
138 | redirect( action: "index" ); |
---|
139 | return; |
---|
140 | } |
---|
141 | [searches: searches] |
---|
142 | } |
---|
143 | |
---|
144 | /** |
---|
145 | * Shows a search screen where the user can search within the results of another search |
---|
146 | * @param id queryId of the search to search in |
---|
147 | */ |
---|
148 | def searchIn = { |
---|
149 | def queryIds = params.list( 'id' ); |
---|
150 | queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) } |
---|
151 | |
---|
152 | if( queryIds.size() == 0 ) { |
---|
153 | flash.error = "Incorrect search ID given to search in" |
---|
154 | redirect( action: "list" ); |
---|
155 | return |
---|
156 | } |
---|
157 | |
---|
158 | // Retrieve the searches from session |
---|
159 | def params = [:] |
---|
160 | queryIds.eachWithIndex { queryId, idx -> |
---|
161 | Search s = retrieveSearch( queryId ); |
---|
162 | if( !s ) { |
---|
163 | flash.message = "Specified search " + queryId + " could not be found" |
---|
164 | return; |
---|
165 | } else { |
---|
166 | params[ "criteria." + idx + ".entityfield" ] = s.entity; |
---|
167 | params[ "criteria." + idx + ".operator" ] = "in"; |
---|
168 | params[ "criteria." + idx + ".value" ] = queryId; |
---|
169 | } |
---|
170 | } |
---|
171 | |
---|
172 | redirect( action: "index", params: params) |
---|
173 | } |
---|
174 | |
---|
175 | /** |
---|
176 | * Combines the results of multiple searches |
---|
177 | * @param id queryIds of the searches to combine |
---|
178 | */ |
---|
179 | def combine = { |
---|
180 | def queryIds = params.list( 'id' ); |
---|
181 | queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) } |
---|
182 | |
---|
183 | if( queryIds.size() == 0 ) { |
---|
184 | flash.error = "Incorrect search ID given to combine" |
---|
185 | redirect( action: "index" ); |
---|
186 | return |
---|
187 | } |
---|
188 | |
---|
189 | // First determine whether the types match |
---|
190 | def searches = []; |
---|
191 | def type = ""; |
---|
192 | flash.error = ""; |
---|
193 | queryIds.eachWithIndex { queryId, idx -> |
---|
194 | Search s = retrieveSearch( queryId ); |
---|
195 | if( !s ) { |
---|
196 | return; |
---|
197 | } |
---|
198 | |
---|
199 | if( type ) { |
---|
200 | if( type != s.entity ) { |
---|
201 | flash.error = type + " and " + s.entity.toLowerCase() + " queries can't be combined. Selected queries of one type."; |
---|
202 | return |
---|
203 | } |
---|
204 | } else { |
---|
205 | type = s.entity |
---|
206 | } |
---|
207 | } |
---|
208 | |
---|
209 | if( flash.error ) { |
---|
210 | redirect( action: "list" ); |
---|
211 | return; |
---|
212 | } |
---|
213 | |
---|
214 | if( !type ) { |
---|
215 | flash.error = "No correct query ids were given." |
---|
216 | redirect( action: "list" ); |
---|
217 | return; |
---|
218 | } |
---|
219 | |
---|
220 | // Retrieve the searches from session |
---|
221 | Search combined = determineSearch( type ); |
---|
222 | combined.searchMode = SearchMode.or; |
---|
223 | |
---|
224 | queryIds.eachWithIndex { queryId, idx -> |
---|
225 | Search s = retrieveSearch( queryId ); |
---|
226 | if( s ) { |
---|
227 | combined.addCriterion( new Criterion( entity: type, field: null, operator: Operator.insearch, value: s ) ); |
---|
228 | } |
---|
229 | } |
---|
230 | |
---|
231 | // Execute search to combine the results |
---|
232 | combined.execute(); |
---|
233 | |
---|
234 | def queryId = saveSearch( combined ); |
---|
235 | redirect( action: "show", id: queryId ); |
---|
236 | } |
---|
237 | |
---|
238 | /** |
---|
239 | * Registers a search from a module with GSCF, in order to be able to refine the searches |
---|
240 | */ |
---|
241 | def refineExternal = { |
---|
242 | // Determine parameters and retrieve objects based on the tokens |
---|
243 | def name = params.name |
---|
244 | def url = params.url |
---|
245 | def entity = params.entity |
---|
246 | def tokens = params.list( 'tokens' ); |
---|
247 | def results |
---|
248 | |
---|
249 | switch( entity ) { |
---|
250 | case "Study": |
---|
251 | results = Study.findAll( "from Study s where s.studyUUID IN (:tokens)", [ 'tokens': tokens ] ) |
---|
252 | break; |
---|
253 | case "Assay": |
---|
254 | results = Assay.findAll( "from Assay a where a.assayUUID IN (:tokens)", [ 'tokens': tokens ] ) |
---|
255 | break; |
---|
256 | case "Sample": |
---|
257 | results = Sample.findAll( "from Sample s where s.sampleUUID IN (:tokens)", [ 'tokens': tokens ] ) |
---|
258 | break; |
---|
259 | default: |
---|
260 | response.sendError( 400 ); |
---|
261 | render "The given entity is not supported. Choose one of Study, Assay or Sample" |
---|
262 | return; |
---|
263 | } |
---|
264 | |
---|
265 | // Register and save search |
---|
266 | Search s = Search.register( name, url, entity, results ); |
---|
267 | int searchId = saveSearch( s ); |
---|
268 | |
---|
269 | // Redirect to the search screen |
---|
270 | def params = [ |
---|
271 | "criteria.0.entityfield": s.entity, |
---|
272 | "criteria.0.operator": "in", |
---|
273 | "criteria.0.value": searchId |
---|
274 | ]; |
---|
275 | |
---|
276 | redirect( action: "index", params: params) |
---|
277 | } |
---|
278 | |
---|
279 | /** |
---|
280 | * Retrieves a list of distinct values that have been entered for the given field. |
---|
281 | */ |
---|
282 | def getFieldValues = { |
---|
283 | def entityField = params.entityfield; |
---|
284 | entityField = entityField.split( /\./ ); |
---|
285 | def entity = entityField[ 0 ]; |
---|
286 | def field = entityField[ 1 ]; |
---|
287 | |
---|
288 | def term = params.term |
---|
289 | def termLike = "%" + term + "%" |
---|
290 | |
---|
291 | // Skip searching all fields |
---|
292 | if( entity == "*" || field == "*" ) { |
---|
293 | render [] as JSON |
---|
294 | return; |
---|
295 | } |
---|
296 | |
---|
297 | def entityClass = TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entity) |
---|
298 | |
---|
299 | // Domain fields can be easily found |
---|
300 | if( field == "Template" ) { |
---|
301 | render Template.executeQuery( "select distinct t.name FROM Template t where t.entity = :entity AND t.name LIKE :term", [ "entity": entityClass, "term": termLike ] ) as JSON |
---|
302 | return; |
---|
303 | } |
---|
304 | |
---|
305 | // Determine domain fields of the entity |
---|
306 | def domainFields = entityClass.giveDomainFields(); |
---|
307 | def domainField = domainFields.find { it.name == field }; |
---|
308 | |
---|
309 | // The values of a domainfield can be determined easily |
---|
310 | if( domainField ) { |
---|
311 | render entityClass.executeQuery( "select distinct e." + field + " FROM " + entity + " e WHERE e." + field + " LIKE :term", [ "term": termLike ] ) as JSON |
---|
312 | return; |
---|
313 | } |
---|
314 | |
---|
315 | // Find all fields with this name and entity, in order to determine the type of the field |
---|
316 | def fields = TemplateField.findAll( "FROM TemplateField t WHERE t.name = :name AND t.entity = :entity", [ "name": field, "entity": entityClass ] ); |
---|
317 | |
---|
318 | // If the field is not found, return an empty list |
---|
319 | def listValues = []; |
---|
320 | if( !fields ) { |
---|
321 | render listValues as JSON |
---|
322 | } |
---|
323 | |
---|
324 | // Determine the type (or types) of the field |
---|
325 | def fieldTypes = fields*.type.unique()*.casedName; |
---|
326 | |
---|
327 | // Now create a list of possible values, based on the fieldType(s) |
---|
328 | |
---|
329 | // Several types of fields are handled differently. |
---|
330 | // The 'simple' types (string, double) are handled by searching in the associated 'templateXXXXFields' table |
---|
331 | // The 'complex' types (stringlist, template etc., referencing another database table) can't be |
---|
332 | // handled correctly (the same way), since the HQL INDEX() function doesn't work on those relations. |
---|
333 | // We do a search for these types to see whether any field with that type fits this criterion, in order to |
---|
334 | // filter out false positives later on. |
---|
335 | fieldTypes.each { type -> |
---|
336 | // Determine field name |
---|
337 | def fieldName = "template" + type + 'Fields' |
---|
338 | |
---|
339 | switch( type ) { |
---|
340 | case 'String': |
---|
341 | case 'Text': |
---|
342 | case 'File': |
---|
343 | // 'Simple' field types (string values) |
---|
344 | listValues += entityClass.executeQuery( "SELECT DISTINCT f FROM " + entity + " s left join s." + fieldName + " f WHERE index(f) = :field AND f LIKE :term", [ "field": field, "term": termLike ] ); |
---|
345 | break; |
---|
346 | |
---|
347 | case 'Date': |
---|
348 | case 'Double': |
---|
349 | case 'Long': |
---|
350 | // 'Simple' field types that can be converted to string and compared |
---|
351 | listValues += entityClass.executeQuery( "SELECT DISTINCT f FROM " + entity + " s left join s." + fieldName + " f WHERE index(f) = :field AND str(f) LIKE :term", [ "field": field, "term": termLike ] ); |
---|
352 | break; |
---|
353 | case 'Boolean': |
---|
354 | // Simple field types that don't support like |
---|
355 | listValues += entityClass.executeQuery( "SELECT DISTINCT f FROM " + entity + " s left join s." + fieldName + " f WHERE index(f) = :field", [ "field": field ] ); |
---|
356 | break; |
---|
357 | |
---|
358 | case 'RelTime': |
---|
359 | // RelTime values should be formatted before returning |
---|
360 | def reltimes = entityClass.executeQuery( "SELECT DISTINCT f FROM " + entity + " s left join s." + fieldName + " f WHERE index(f) = :field", [ "field": field ] ); |
---|
361 | listValues += reltimes.collect { def rt = new RelTime( it ); return rt.toString(); } |
---|
362 | break; |
---|
363 | |
---|
364 | case 'StringList': |
---|
365 | case 'ExtendableStringList': |
---|
366 | case 'Term': |
---|
367 | case 'Template': |
---|
368 | case 'Module': |
---|
369 | // 'Complex' field types: select all possible names for the given field, that have ever been used |
---|
370 | // (i.e. all ontologies that have ever been used in any field). We have to do it this way, because the HQL |
---|
371 | // index() function (see simple fields) doesn't work on many-to-many relations |
---|
372 | listValues += entityClass.executeQuery( "SELECT DISTINCT f.name FROM " + entity + " s left join s." + fieldName + " f WHERE f.name LIKE :term", [ "term": termLike ] ); |
---|
373 | default: |
---|
374 | break; |
---|
375 | } |
---|
376 | } |
---|
377 | |
---|
378 | render listValues as JSON |
---|
379 | } |
---|
380 | |
---|
381 | protected String determineView( String entity ) { |
---|
382 | switch( entity ) { |
---|
383 | case "Study": return "studyresults"; break; |
---|
384 | case "Sample": return "sampleresults"; break; |
---|
385 | case "Assay": return "assayresults"; break; |
---|
386 | default: return "results"; break; |
---|
387 | } |
---|
388 | } |
---|
389 | |
---|
390 | /** |
---|
391 | * Returns the search object used for searching |
---|
392 | */ |
---|
393 | protected Search determineSearch( String entity ) { |
---|
394 | switch( entity ) { |
---|
395 | case "Study": return new StudySearch(); |
---|
396 | case "Sample": return new SampleSearch(); |
---|
397 | case "Assay": return new AssaySearch(); |
---|
398 | |
---|
399 | // This exception will only be thrown if the entitiesToSearchFor contains more entities than |
---|
400 | // mentioned in this switch structure. |
---|
401 | default: throw new Exception( "Can't search for entities of type " + entity ); |
---|
402 | } |
---|
403 | } |
---|
404 | |
---|
405 | /** |
---|
406 | * Returns a map of entities with the names of the fields the user can search on |
---|
407 | * @return |
---|
408 | */ |
---|
409 | protected def getSearchableFields() { |
---|
410 | def fields = [ '*' : [ '*' ] ]; // Searches for all fields in all objects |
---|
411 | |
---|
412 | // Retrieve all local search fields |
---|
413 | getEntities().each { |
---|
414 | def entity = getEntity( 'dbnp.studycapturing.' + it ); |
---|
415 | |
---|
416 | if( entity ) { |
---|
417 | def domainFields = entity.giveDomainFields(); |
---|
418 | def templateFields = TemplateField.findAllByEntity( entity ) |
---|
419 | |
---|
420 | def fieldNames = ( domainFields + templateFields ).collect { it.name }.unique() + 'Template' + '*' |
---|
421 | |
---|
422 | fields[ it ] = fieldNames.sort { a, b -> |
---|
423 | def aUC = a.size() > 1 ? a[0].toUpperCase() + a[1..-1] : a; |
---|
424 | def bUC = b.size() > 1 ? b[0].toUpperCase() + b[1..-1] : b; |
---|
425 | aUC <=> bUC |
---|
426 | }; |
---|
427 | } |
---|
428 | } |
---|
429 | |
---|
430 | // Loop through all modules and check which fields are searchable |
---|
431 | // Right now, we just combine the results for different entities |
---|
432 | AssayModule.list().each { module -> |
---|
433 | def callUrl = module.url + '/rest/getQueryableFields' |
---|
434 | try { |
---|
435 | def json = moduleCommunicationService.callModuleMethod( module.url, callUrl ); |
---|
436 | def moduleFields = []; |
---|
437 | entitiesToSearchFor.each { entity -> |
---|
438 | if( json[ entity.key ] ) { |
---|
439 | json[ entity.key ].each { field -> |
---|
440 | moduleFields << field.toString(); |
---|
441 | } |
---|
442 | } |
---|
443 | } |
---|
444 | |
---|
445 | // Remove 'module' from module name |
---|
446 | def moduleName = module.name.replace( 'module', '' ).trim() |
---|
447 | |
---|
448 | fields[ moduleName ] = moduleFields.unique() + '*'; |
---|
449 | } catch( Exception e ) { |
---|
450 | log.error( "Error while retrieving queryable fields from " + module.name + ": " + e.getMessage() ) |
---|
451 | } |
---|
452 | } |
---|
453 | |
---|
454 | return fields; |
---|
455 | } |
---|
456 | |
---|
457 | /** |
---|
458 | * Parses the criteria from the query form given by the user |
---|
459 | * @param c Data from the input form and had a form like |
---|
460 | * |
---|
461 | * [ |
---|
462 | * 0: [entityfield:a.b, operator: b, value: c], |
---|
463 | * 0.entityfield: a.b, |
---|
464 | * 0.operator: b, |
---|
465 | * 0.field: c |
---|
466 | * 1: [entityfield:f.q, operator: e, value: d], |
---|
467 | * 1.entityfield: f.q, |
---|
468 | * 1.operator: e, |
---|
469 | * 1.field: d |
---|
470 | * ] |
---|
471 | * @param parseSearchIds Determines whether searches are returned instead of their ids |
---|
472 | * @return List with Criterion objects |
---|
473 | */ |
---|
474 | protected List parseCriteria( def formCriteria, def parseSearchIds = true ) { |
---|
475 | ArrayList list = []; |
---|
476 | flash.error = ""; |
---|
477 | |
---|
478 | // Loop through all keys of c and remove the non-numeric ones |
---|
479 | for( c in formCriteria ) { |
---|
480 | if( c.key ==~ /[0-9]+/ && c.value.entityfield ) { |
---|
481 | def formCriterion = c.value; |
---|
482 | |
---|
483 | Criterion criterion = new Criterion(); |
---|
484 | |
---|
485 | // Split entity and field |
---|
486 | def field = formCriterion.entityfield?.split( /\./ ); |
---|
487 | if( field.size() > 1 ) { |
---|
488 | criterion.entity = field[0].toString(); |
---|
489 | criterion.field = field[1].toString(); |
---|
490 | } else { |
---|
491 | criterion.entity = field[0]; |
---|
492 | criterion.field = null; |
---|
493 | } |
---|
494 | |
---|
495 | // Convert operator string to Operator-enum field |
---|
496 | try { |
---|
497 | criterion.operator = Criterion.parseOperator( formCriterion.operator ); |
---|
498 | } catch( Exception e) { |
---|
499 | log.debug "Operator " + formCriterion.operator + " could not be parsed: " + e.getMessage(); |
---|
500 | flash.error += "Criterion could not be used: operator " + formCriterion.operator + " is not valid.<br />\n"; |
---|
501 | continue; |
---|
502 | } |
---|
503 | |
---|
504 | // Special case of the 'in' operator |
---|
505 | if( criterion.operator == Operator.insearch ) { |
---|
506 | Search s |
---|
507 | try { |
---|
508 | s = retrieveSearch( Integer.parseInt( formCriterion.value ) ); |
---|
509 | } catch( Exception e ) {} |
---|
510 | |
---|
511 | if( !s ) { |
---|
512 | flash.error += "Can't search within previous query: query not found"; |
---|
513 | continue; |
---|
514 | } |
---|
515 | |
---|
516 | if( parseSearchIds ) { |
---|
517 | criterion.value = s |
---|
518 | } else { |
---|
519 | criterion.value = s.id |
---|
520 | } |
---|
521 | } else { |
---|
522 | // Copy value |
---|
523 | criterion.value = formCriterion.value; |
---|
524 | } |
---|
525 | |
---|
526 | list << criterion; |
---|
527 | } |
---|
528 | } |
---|
529 | |
---|
530 | return list; |
---|
531 | } |
---|
532 | |
---|
533 | /** |
---|
534 | * Returns all entities for which criteria can be entered |
---|
535 | * @return |
---|
536 | */ |
---|
537 | protected def getEntities() { |
---|
538 | return [ 'Study', 'Subject', 'Sample', 'Event', 'SamplingEvent', 'Assay' ] |
---|
539 | } |
---|
540 | |
---|
541 | /** |
---|
542 | * Creates an object of the given entity. |
---|
543 | * |
---|
544 | * @return False if the entity is not a subclass of TemplateEntity |
---|
545 | */ |
---|
546 | protected def getEntity( entityName ) { |
---|
547 | // Find the templates |
---|
548 | def entity |
---|
549 | try { |
---|
550 | entity = Class.forName(entityName, true, this.getClass().getClassLoader()) |
---|
551 | |
---|
552 | // succes, is entity an instance of TemplateEntity? |
---|
553 | if (entity.superclass =~ /TemplateEntity$/ || entity.superclass.superclass =~ /TemplateEntity$/) { |
---|
554 | return entity; |
---|
555 | } else { |
---|
556 | return false; |
---|
557 | } |
---|
558 | } catch( ClassNotFoundException e ) { |
---|
559 | log.error "Class " + entityName + " not found: " + e.getMessage() |
---|
560 | return null; |
---|
561 | } |
---|
562 | |
---|
563 | } |
---|
564 | |
---|
565 | |
---|
566 | /*************************************************************************** |
---|
567 | * |
---|
568 | * Methods for saving results in session |
---|
569 | * |
---|
570 | ***************************************************************************/ |
---|
571 | |
---|
572 | /** |
---|
573 | * Saves the given search in session. Any search with the same criteria will be overwritten |
---|
574 | * |
---|
575 | * @param s Search to save |
---|
576 | * @return Id of the search for later reference |
---|
577 | */ |
---|
578 | protected int saveSearch( Search s ) { |
---|
579 | if( !session.queries ) |
---|
580 | session.queries = [:] |
---|
581 | |
---|
582 | // First check whether a search with the same criteria is already present |
---|
583 | def previousSearch = retrieveSearch( s ); |
---|
584 | |
---|
585 | def id |
---|
586 | if( previousSearch ) { |
---|
587 | id = previousSearch.id; |
---|
588 | } else { |
---|
589 | // Determine unique id |
---|
590 | id = ( session.queries*.key.max() ?: 0 ) + 1; |
---|
591 | } |
---|
592 | |
---|
593 | s.id = id; |
---|
594 | |
---|
595 | if( !s.url ) |
---|
596 | s.url = g.createLink( controller: "advancedQuery", action: "show", id: id, absolute: true ); |
---|
597 | |
---|
598 | session.queries[ id ] = s; |
---|
599 | |
---|
600 | return id; |
---|
601 | } |
---|
602 | |
---|
603 | /** |
---|
604 | * Retrieves a search from session with the same criteria as given |
---|
605 | * @param s Search that is used as an example to search for |
---|
606 | * @return Search that has this criteria, or null if no such search is found. |
---|
607 | */ |
---|
608 | protected Search retrieveSearch( Search s ) { |
---|
609 | if( !session.queries ) |
---|
610 | return null |
---|
611 | |
---|
612 | for( query in session.queries ) { |
---|
613 | def value = query.value; |
---|
614 | |
---|
615 | if( s.equals( value ) ) |
---|
616 | return value |
---|
617 | } |
---|
618 | |
---|
619 | return null; |
---|
620 | } |
---|
621 | |
---|
622 | |
---|
623 | /** |
---|
624 | * Retrieves a search from session |
---|
625 | * @param id Id of the search |
---|
626 | * @return Search that belongs to this ID or null if no search is found |
---|
627 | */ |
---|
628 | protected Search retrieveSearch( int id ) { |
---|
629 | if( !session.queries || !session.queries[ id ] ) |
---|
630 | return null |
---|
631 | |
---|
632 | if( !( session.queries[ id ] instanceof Search ) ) |
---|
633 | return null; |
---|
634 | |
---|
635 | return (Search) session.queries[ id ] |
---|
636 | } |
---|
637 | |
---|
638 | /** |
---|
639 | * Removes a search from session |
---|
640 | * @param id Id of the search |
---|
641 | * @return Search that belonged to this ID or null if no search is found |
---|
642 | */ |
---|
643 | protected Search discardSearch( int id ) { |
---|
644 | if( !session.queries || !session.queries[ id ] ) |
---|
645 | return null |
---|
646 | |
---|
647 | def sessionSearch = session.queries[ id ]; |
---|
648 | |
---|
649 | session.queries.remove( id ); |
---|
650 | |
---|
651 | if( !( sessionSearch instanceof Search ) ) |
---|
652 | return null; |
---|
653 | |
---|
654 | return (Search) sessionSearch |
---|
655 | } |
---|
656 | |
---|
657 | /** |
---|
658 | * Retrieves a list of searches from session |
---|
659 | * @return List of searches from session |
---|
660 | */ |
---|
661 | protected List listSearches() { |
---|
662 | if( !session.queries ) |
---|
663 | return [] |
---|
664 | |
---|
665 | return session.queries*.value.toList() |
---|
666 | } |
---|
667 | |
---|
668 | /** |
---|
669 | * Determine a list of actions that can be performed on specific entities |
---|
670 | * @param entity Name of the entity that the actions could be performed on |
---|
671 | * @param selectedIds List with ids of the selected items to perform an action on |
---|
672 | * @return |
---|
673 | */ |
---|
674 | protected List determineActions( Search s, def selectedIds = null ) { |
---|
675 | return gscfActions( s, selectedIds ) + moduleActions( s, selectedIds ); |
---|
676 | } |
---|
677 | |
---|
678 | /** |
---|
679 | * Determine a list of actions that can be performed on specific entities by GSCF |
---|
680 | * @param entity Name of the entity that the actions could be performed on |
---|
681 | * @param selectedTokens List with tokens (UUID) of the selected items to perform an action on |
---|
682 | */ |
---|
683 | protected List gscfActions(Search s, def selectedTokens = null) { |
---|
684 | switch(s.entity) { |
---|
685 | case "Study": |
---|
686 | def ids = [] |
---|
687 | s.filterResults(selectedTokens).each { |
---|
688 | ids << it.id |
---|
689 | } |
---|
690 | |
---|
691 | def paramString = ids.collect { return 'ids=' + it }.join( '&' ); |
---|
692 | |
---|
693 | return [[ |
---|
694 | module: "gscf", |
---|
695 | name:"simpletox", |
---|
696 | type: "export", |
---|
697 | description: "Export as SimpleTox", |
---|
698 | url: createLink( controller: "exporter", action: "export", params: [ 'format': 'list', 'ids' : ids ] ), |
---|
699 | submitUrl: createLink( controller: "exporter", action: "export", params: [ 'format': 'list' ] ), |
---|
700 | paramString: paramString |
---|
701 | ], [ |
---|
702 | module: "gscf", |
---|
703 | name:"excel", |
---|
704 | type: "export", |
---|
705 | description: "Export as CSV", |
---|
706 | url: createLink( controller: "study", action: "exportToExcel", params: [ 'format': 'list', 'ids' : ids ] ), |
---|
707 | submitUrl: createLink( controller: "study", action: "exportToExcel", params: [ 'format': 'list' ] ), |
---|
708 | paramString: paramString |
---|
709 | ]] |
---|
710 | case "Assay": |
---|
711 | def ids = [] |
---|
712 | s.filterResults(selectedTokens).each { |
---|
713 | ids << it.id |
---|
714 | } |
---|
715 | |
---|
716 | def paramString = ids.collect { return 'ids=' + it }.join( '&' ); |
---|
717 | |
---|
718 | return [[ |
---|
719 | module: "gscf", |
---|
720 | name:"excel", |
---|
721 | type: "export", |
---|
722 | description: "Export as CSV", |
---|
723 | url: createLink( controller: "assay", action: "exportToExcel", params: [ 'format': 'list', 'ids' : ids ] ), |
---|
724 | submitUrl: createLink( controller: "assay", action: "exportToExcel", params: [ 'format': 'list' ] ), |
---|
725 | paramString: paramString |
---|
726 | ]] |
---|
727 | case "Sample": |
---|
728 | def ids = [] |
---|
729 | s.filterResults(selectedTokens).each { |
---|
730 | ids << it.id |
---|
731 | } |
---|
732 | |
---|
733 | def paramString = ids.collect { return 'ids=' + it }.join( '&' ); |
---|
734 | |
---|
735 | return [[ |
---|
736 | module: "gscf", |
---|
737 | name:"excel", |
---|
738 | type: "export", |
---|
739 | description: "Export as CSV", |
---|
740 | url: createLink( controller: "assay", action: "exportToSamplesToCsv", params: [ 'ids' : ids ] ), |
---|
741 | submitUrl: createLink( controller: "assay", action: "exportSamplesToCsv" ), |
---|
742 | paramString: paramString |
---|
743 | ]] |
---|
744 | default: |
---|
745 | return []; |
---|
746 | } |
---|
747 | } |
---|
748 | |
---|
749 | /** |
---|
750 | * Determine a list of actions that can be performed on specific entities by other modules |
---|
751 | * @param entity Name of the entity that the actions could be performed on |
---|
752 | */ |
---|
753 | protected List moduleActions(Search s, def selectedTokens = null) { |
---|
754 | def actions = [] |
---|
755 | |
---|
756 | if( !s.getResults() || s.getResults().size() == 0 ) |
---|
757 | return [] |
---|
758 | |
---|
759 | // Loop through all modules and check which actions can be performed on the |
---|
760 | AssayModule.list().each { module -> |
---|
761 | // Remove 'module' from module name |
---|
762 | def moduleName = module.name.replace( 'module', '' ).trim() |
---|
763 | try { |
---|
764 | def callUrl = module.url + "/rest/getPossibleActions?entity=" + s.entity |
---|
765 | def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl ); |
---|
766 | |
---|
767 | // Check whether the entity is present in the return value |
---|
768 | if( json[ s.entity ] ) { |
---|
769 | json[ s.entity ].each { action -> |
---|
770 | def baseUrl = action.url ?: module.url + "/action/" + action.name |
---|
771 | def paramString = s.filterResults(selectedTokens).collect { "tokens=" + it.giveUUID() }.join( "&" ) |
---|
772 | |
---|
773 | def url = baseUrl; |
---|
774 | |
---|
775 | if( url.find( /\?/ ) ) |
---|
776 | url += "&" |
---|
777 | else |
---|
778 | url += "?" |
---|
779 | |
---|
780 | paramString += "&entity=" + s.entity |
---|
781 | |
---|
782 | actions << [ |
---|
783 | module: moduleName, |
---|
784 | name: action.name, |
---|
785 | type: action.type ?: 'default', |
---|
786 | description: action.description + " (" + moduleName + ")", |
---|
787 | url: url + "&" + paramString, |
---|
788 | submitUrl: baseUrl, |
---|
789 | paramString: paramString |
---|
790 | ]; |
---|
791 | } |
---|
792 | } |
---|
793 | } catch( Exception e ) { |
---|
794 | // Exception is thrown when the call to the module fails. No problems though. |
---|
795 | log.error "Error while fetching possible actions from " + module.name + ": " + e.getMessage() |
---|
796 | } |
---|
797 | } |
---|
798 | |
---|
799 | return actions; |
---|
800 | } |
---|
801 | |
---|
802 | } |
---|