1 | package dbnp.query |
---|
2 | |
---|
3 | import java.text.SimpleDateFormat |
---|
4 | import org.dbnp.gdt.* |
---|
5 | import dbnp.studycapturing.* |
---|
6 | import org.apache.commons.logging.LogFactory; |
---|
7 | |
---|
8 | /** |
---|
9 | * Available operators for criteria |
---|
10 | * @author robert |
---|
11 | * |
---|
12 | */ |
---|
13 | enum Operator { |
---|
14 | equals( "=" ), contains( "contains" ), gte( ">="), gt( ">" ), lte( "<=" ), lt( "<" ), insearch( "in" ) |
---|
15 | Operator(String name) { this.name = name } |
---|
16 | private final String name; |
---|
17 | public String toString() { return name } |
---|
18 | } |
---|
19 | |
---|
20 | /** |
---|
21 | * Represents a criterion to search on |
---|
22 | * @author robert |
---|
23 | * |
---|
24 | */ |
---|
25 | class Criterion { |
---|
26 | private static final log = LogFactory.getLog(this); |
---|
27 | public String entity |
---|
28 | public String field |
---|
29 | public Operator operator |
---|
30 | public def value |
---|
31 | |
---|
32 | /** |
---|
33 | * Returns the class for the entity of this criterion |
---|
34 | * @return |
---|
35 | */ |
---|
36 | public Class entityClass() { |
---|
37 | if( this.entity == '*' ) |
---|
38 | return null; |
---|
39 | |
---|
40 | |
---|
41 | try { |
---|
42 | return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity) |
---|
43 | } catch( Exception e ) { |
---|
44 | throw new Exception( "Unknown entity for criterion " + this, e ); |
---|
45 | } |
---|
46 | } |
---|
47 | |
---|
48 | /** |
---|
49 | * Retrieves a combination of the entity and field |
---|
50 | * @return |
---|
51 | */ |
---|
52 | public String entityField() { |
---|
53 | return entity.toString() + ( field ? "." + field.toString() : "" ); |
---|
54 | } |
---|
55 | |
---|
56 | /** |
---|
57 | * Retrieves a human readable description of the combination of the entity and field |
---|
58 | * @return |
---|
59 | */ |
---|
60 | public String humanReadableEntityField() { |
---|
61 | if( field == '*' ) { |
---|
62 | if( entity == '*' ) { |
---|
63 | return "any field in any object" |
---|
64 | } else { |
---|
65 | return "any field in " + entity.toString(); |
---|
66 | } |
---|
67 | } else { |
---|
68 | return entityField(); |
---|
69 | } |
---|
70 | } |
---|
71 | |
---|
72 | /** |
---|
73 | * Returns the type of criterion when searching. Multiple types can be returned, since fields |
---|
74 | * with the same name might have different types. |
---|
75 | * |
---|
76 | * @return List of strings determining the type of this criterion. Possibilities are: |
---|
77 | * [STRING,BOOLEAN,..]:The criterion references a template field that contains a 'simple' |
---|
78 | * value (boolean, double, long, string, reltime, date) |
---|
79 | * [STRINGLIST,...]: The criterion references a template field that contains a 'complex' |
---|
80 | * value (listitem, ontologyterm, template, module) referencing another |
---|
81 | * database table |
---|
82 | * Wildcard: The criterion references all fields |
---|
83 | */ |
---|
84 | protected List<String> criterionType() { |
---|
85 | if( this.entity == '*' || this.field == '*' ) { |
---|
86 | return [ |
---|
87 | 'String', |
---|
88 | 'Text', |
---|
89 | 'File', |
---|
90 | 'Date', |
---|
91 | 'RelTime', |
---|
92 | 'Double', |
---|
93 | 'Long', |
---|
94 | 'Boolean', |
---|
95 | 'StringList', |
---|
96 | 'ExtendableStringList', |
---|
97 | 'Term', |
---|
98 | 'Template', |
---|
99 | 'Module' |
---|
100 | ] |
---|
101 | } |
---|
102 | |
---|
103 | // Template fields are string fields |
---|
104 | if( this.field == 'Template' ) |
---|
105 | return [ "String" ] |
---|
106 | |
---|
107 | // Determine domain fields of the entity |
---|
108 | def domainFields = entityClass().giveDomainFields(); |
---|
109 | def domainField = domainFields.find { it.name == this.field }; |
---|
110 | if( domainField ) |
---|
111 | return [domainField.type?.casedName]; |
---|
112 | |
---|
113 | // If this field is not a domain field, search for the field in the database |
---|
114 | def entityClass = entityClass() |
---|
115 | |
---|
116 | if( !entityClass || !this.field ) |
---|
117 | return null; |
---|
118 | |
---|
119 | // Find all fields with this name and entity |
---|
120 | def fields = TemplateField.findAllByName( this.field ).findAll { it.entity == entityClass }; |
---|
121 | |
---|
122 | // If the field is not found, return null |
---|
123 | if( !fields ) |
---|
124 | return null |
---|
125 | |
---|
126 | // Return the (unique) String value of the types |
---|
127 | return fields*.type.unique()*.casedName; |
---|
128 | } |
---|
129 | |
---|
130 | /** |
---|
131 | * Determines whether the field in this criterion is a domain field |
---|
132 | * |
---|
133 | * @return True iff the field is a domain field, false otherwise |
---|
134 | */ |
---|
135 | protected boolean isDomainCriterion() { |
---|
136 | def entityClass = entityClass() |
---|
137 | |
---|
138 | if( !entityClass ) |
---|
139 | return false; |
---|
140 | |
---|
141 | // Template fields should be handled as domain criteria |
---|
142 | if( this.field == "Template" ) |
---|
143 | return true; |
---|
144 | |
---|
145 | // Determine domain fields of the entity |
---|
146 | def domainFields = entityClass.giveDomainFields(); |
---|
147 | def domainField = domainFields.find { it.name == this.field }; |
---|
148 | |
---|
149 | return (domainField ? true : false) |
---|
150 | } |
---|
151 | |
---|
152 | /** |
---|
153 | * Determines whether this criterion references a 'complex' field (i.e. a field that |
---|
154 | * contains a complex type like Term, ListItem etc.) |
---|
155 | * |
---|
156 | * @return |
---|
157 | */ |
---|
158 | public boolean isComplexCriterion() { |
---|
159 | if( this.field == '*' ) |
---|
160 | return false; |
---|
161 | |
---|
162 | if( isDomainCriterion() ) |
---|
163 | return false; |
---|
164 | |
---|
165 | def types = criterionType(); |
---|
166 | |
---|
167 | return types.any { type -> |
---|
168 | switch( type ) { |
---|
169 | case 'StringList': |
---|
170 | case 'ExtendableStringList': |
---|
171 | case 'Term': |
---|
172 | case 'Template': |
---|
173 | case 'Module': |
---|
174 | return true; |
---|
175 | } |
---|
176 | |
---|
177 | return false; |
---|
178 | } |
---|
179 | } |
---|
180 | |
---|
181 | /** |
---|
182 | * Case the field value to search on to the given type |
---|
183 | * @param fieldType Name of the template field type |
---|
184 | * @return Value casted to the right value |
---|
185 | */ |
---|
186 | protected def castValue( String fieldType ) { |
---|
187 | switch( fieldType ) { |
---|
188 | |
---|
189 | case 'String': |
---|
190 | case 'Text': |
---|
191 | case 'StringList': |
---|
192 | case 'ExtendableStringList': |
---|
193 | case 'Term': |
---|
194 | case 'Template': |
---|
195 | case 'Module': |
---|
196 | return value?.toString(); |
---|
197 | case 'File': |
---|
198 | return null; // Never search in filenames, since they are not very descriptive |
---|
199 | case 'Date': |
---|
200 | // The comparison with date values should only be performed iff the value |
---|
201 | // contains a parsable date |
---|
202 | // and the operator is equals, gte, gt, lt or lte |
---|
203 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
204 | return null |
---|
205 | |
---|
206 | try { |
---|
207 | Date dateCriterion = new SimpleDateFormat( "yyyy-MM-dd" ).parse( value ); |
---|
208 | return dateCriterion |
---|
209 | } catch( Exception e ) { |
---|
210 | return null; |
---|
211 | } |
---|
212 | |
---|
213 | case 'RelTime': |
---|
214 | // The comparison with date values should only be performed iff the value |
---|
215 | // contains a long number |
---|
216 | // and the operator is equals, gte, gt, lt or lte |
---|
217 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
218 | return null |
---|
219 | |
---|
220 | try { |
---|
221 | RelTime rt |
---|
222 | |
---|
223 | // Numbers are taken to be seconds, if a non-numeric value is given, try to parse it |
---|
224 | if( value.toString().isLong() ) { |
---|
225 | rt = new RelTime( Long.parseLong( value.toString() ) ); |
---|
226 | } else { |
---|
227 | rt = new RelTime( value.toString() ); |
---|
228 | } |
---|
229 | |
---|
230 | return rt.getValue() |
---|
231 | } catch( Exception e ) { |
---|
232 | return null; |
---|
233 | } |
---|
234 | case 'Double': |
---|
235 | // The comparison with date values should only be performed iff the value |
---|
236 | // contains a double number |
---|
237 | // and the operator is equals, gte, gt, lt or lte |
---|
238 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
239 | return null |
---|
240 | |
---|
241 | if( value.isDouble() ) { |
---|
242 | return Double.parseDouble( value ) |
---|
243 | } else { |
---|
244 | return null; |
---|
245 | } |
---|
246 | case 'Long': |
---|
247 | // The comparison with date values should only be performed iff the value |
---|
248 | // contains a long number |
---|
249 | // and the operator is equals, gte, gt, lt or lte |
---|
250 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
251 | return null |
---|
252 | |
---|
253 | if( value.isLong() ) { |
---|
254 | return Long.parseLong( value ) |
---|
255 | } else { |
---|
256 | return null; |
---|
257 | } |
---|
258 | case 'Boolean': |
---|
259 | // The comparison with boolean values should only be performed iff the value |
---|
260 | // contains 'true' or 'false' (case insensitive) |
---|
261 | // and the operator is equals |
---|
262 | if( operator != Operator.equals ) |
---|
263 | return null |
---|
264 | |
---|
265 | def lowerCaseValue = value.toString().toLowerCase(); |
---|
266 | if( lowerCaseValue == 'true' || lowerCaseValue == 'false' ) { |
---|
267 | return Boolean.parseBoolean( this.value ) |
---|
268 | } else { |
---|
269 | return null |
---|
270 | } |
---|
271 | } |
---|
272 | } |
---|
273 | |
---|
274 | /** |
---|
275 | * Create a HQL where clause from this criterion, in order to be used within a larger HQL statement |
---|
276 | * |
---|
277 | * @param objectToSearchIn HQL name of the object to search in |
---|
278 | * @return Map with 3 keys: 'join' and'where' with the HQL join and where clause for this criterion and 'parameters' for the query named parameters |
---|
279 | */ |
---|
280 | public Map toHQL( String prefix, String objectToSearchIn = "object" ) { |
---|
281 | List whereClause = [] |
---|
282 | String joinClause = ""; |
---|
283 | Map parameters = [:]; |
---|
284 | def emptyCriterion = [ "join": null, "where": null, "parameters": null ]; |
---|
285 | |
---|
286 | // If this criterion is used to search within another search result, we use a special piece of HQL |
---|
287 | if( this.operator == Operator.insearch ) { |
---|
288 | if( this.value?.results ) { |
---|
289 | parameters[ prefix + "SearchResults" ] = this.value?.results |
---|
290 | |
---|
291 | return [ "join": "", "where": "( " + objectToSearchIn + " in (:" + prefix + "SearchResults) )" , "parameters": parameters ]; |
---|
292 | } else { |
---|
293 | return emptyCriterion; |
---|
294 | } |
---|
295 | } |
---|
296 | |
---|
297 | // If no value is given, don't do anything |
---|
298 | if( !value ) |
---|
299 | return emptyCriterion; |
---|
300 | |
---|
301 | // Check whether the field is a domain field |
---|
302 | if( isDomainCriterion() ) { |
---|
303 | // Determine the types of this criterion, but there will be only 1 for a domain field |
---|
304 | def criterionType = criterionType()[0]; |
---|
305 | |
---|
306 | // Some domain fields don't contain a value, but a reference to another table |
---|
307 | // These should be handled differently |
---|
308 | def fieldName = this.field |
---|
309 | |
---|
310 | // Make sure the Template field is referenced as lowercase |
---|
311 | if( fieldName == "Template" ) |
---|
312 | fieldName = "template"; |
---|
313 | |
---|
314 | if( |
---|
315 | ( fieldName == "template" ) || |
---|
316 | ( objectToSearchIn.toLowerCase() == "subject" && fieldName.toLowerCase() == "species" ) || |
---|
317 | ( objectToSearchIn.toLowerCase() == "sample" && fieldName.toLowerCase() == "material" ) || |
---|
318 | ( objectToSearchIn.toLowerCase() == "assay" && fieldName.toLowerCase() == "module" ) || |
---|
319 | ( objectToSearchIn.toLowerCase() == "samplingevent" && fieldName.toLowerCase() == "sampletemplate" ) ) { |
---|
320 | fieldName += ".name" |
---|
321 | } |
---|
322 | |
---|
323 | def query = extendWhereClause( "( %s )", objectToSearchIn + "." + fieldName, prefix, criterionType, castValue( criterionType ) ); |
---|
324 | return [ "join": "", "where": query.where, "parameters": query.parameters ] |
---|
325 | } |
---|
326 | |
---|
327 | // Determine the type of this criterion |
---|
328 | def criterionTypes = criterionType(); |
---|
329 | |
---|
330 | if( !criterionTypes ) |
---|
331 | return emptyCriterion; |
---|
332 | |
---|
333 | // Several types of criteria are handled differently. |
---|
334 | // The 'wildcard' is handled by searching for all types. |
---|
335 | // The 'simple' types (string, double) are handled by searching in the associated table |
---|
336 | // The 'complex' types (stringlist, template etc., referencing another |
---|
337 | // database table) can't be handled correctly, since the HQL INDEX() function doesn't work on those relations. |
---|
338 | // We do a search for these types to see whether any field with that type fits this criterion, in order to |
---|
339 | // filter out false positives later on. |
---|
340 | criterionTypes.findAll { it }.each { criterionType -> |
---|
341 | // Cast criterion value to the right type |
---|
342 | def currentValue = castValue( criterionType ); |
---|
343 | |
---|
344 | // Determine field name |
---|
345 | def fieldName = "template" + criterionType + 'Fields' |
---|
346 | |
---|
347 | switch( criterionType ) { |
---|
348 | case "Wildcard": |
---|
349 | // Wildcard search is handled by |
---|
350 | break; |
---|
351 | |
---|
352 | case 'String': |
---|
353 | case 'Text': |
---|
354 | case 'File': |
---|
355 | case 'Date': |
---|
356 | case 'RelTime': |
---|
357 | case 'Double': |
---|
358 | case 'Long': |
---|
359 | case 'Boolean': |
---|
360 | // 'Simple' field types |
---|
361 | if( currentValue != null ) { |
---|
362 | joinClause += " left join " + objectToSearchIn + "." + fieldName + " as " + prefix + "_" + fieldName + " "; |
---|
363 | |
---|
364 | def condition = this.oneToManyWhereCondition( prefix + "_" + fieldName, prefix, criterionType, currentValue ) |
---|
365 | whereClause += condition[ "where" ]; |
---|
366 | |
---|
367 | condition[ "parameters" ].each { |
---|
368 | parameters[ it.key ] = it.value; |
---|
369 | } |
---|
370 | } |
---|
371 | break; |
---|
372 | |
---|
373 | case 'StringList': |
---|
374 | case 'ExtendableStringList': |
---|
375 | case 'Term': |
---|
376 | case 'Template': |
---|
377 | case 'Module': |
---|
378 | // 'Complex' field types |
---|
379 | def condition = this.manyToManyWhereCondition( objectToSearchIn, fieldName, prefix, "name", currentValue ) |
---|
380 | whereClause += condition[ "where" ]; |
---|
381 | |
---|
382 | condition[ "parameters" ].each { |
---|
383 | parameters[ it.key ] = it.value; |
---|
384 | } |
---|
385 | default: |
---|
386 | break; |
---|
387 | } |
---|
388 | } |
---|
389 | |
---|
390 | // Wildcard searches must also search the fixed domain fields of all entities. |
---|
391 | if( this.field == '*' ) { |
---|
392 | def condition = wildcardDomainFields( prefix, objectToSearchIn ) |
---|
393 | |
---|
394 | whereClause += condition[ "where" ]; |
---|
395 | condition[ "parameters" ].each { |
---|
396 | parameters[ it.key ] = it.value; |
---|
397 | } |
---|
398 | } |
---|
399 | |
---|
400 | def where = whereClause?.findAll { it } ? "( " + whereClause.join( " OR " ) + " )" : "" |
---|
401 | |
---|
402 | return [ "join": joinClause, "where": where , "parameters": parameters ]; |
---|
403 | } |
---|
404 | |
---|
405 | protected Map wildcardDomainFields( String prefix, String objectToSearchIn ) { |
---|
406 | def whereClause = []; |
---|
407 | def parameters = [:]; |
---|
408 | |
---|
409 | // Determine all domain fields |
---|
410 | def domainFields |
---|
411 | |
---|
412 | switch( objectToSearchIn.toLowerCase() ) { |
---|
413 | case "study": domainFields = Study.giveDomainFields(); break; |
---|
414 | case "subject": domainFields = Subject.giveDomainFields(); break; |
---|
415 | case "event": domainFields = Event.giveDomainFields(); break; |
---|
416 | case "sample": domainFields = Sample.giveDomainFields(); break; |
---|
417 | case "assay": domainFields = Assay.giveDomainFields(); break; |
---|
418 | case "samplingevent": domainFields = SamplingEvent.giveDomainFields(); break; |
---|
419 | } |
---|
420 | |
---|
421 | domainFields.each { field -> |
---|
422 | def criterionType = field.type?.casedName; |
---|
423 | |
---|
424 | def fieldName = field.name; |
---|
425 | |
---|
426 | if( ( objectToSearchIn.toLowerCase() == "subject" && fieldName.toLowerCase() == "species" ) || |
---|
427 | ( objectToSearchIn.toLowerCase() == "sample" && fieldName.toLowerCase() == "material" ) || |
---|
428 | ( objectToSearchIn.toLowerCase() == "assay" && fieldName.toLowerCase() == "module" ) || |
---|
429 | ( objectToSearchIn.toLowerCase() == "samplingevent" && fieldName.toLowerCase() == "sampletemplate" ) ) { |
---|
430 | fieldName += ".name" |
---|
431 | } |
---|
432 | |
---|
433 | // Search in template name |
---|
434 | def condition = extendWhereClause( "( %s )", objectToSearchIn + "." + fieldName, prefix, criterionType, castValue( criterionType ) ); |
---|
435 | whereClause += condition[ "where" ]; |
---|
436 | |
---|
437 | condition[ "parameters" ].each { |
---|
438 | parameters[ it.key ] = it.value; |
---|
439 | } |
---|
440 | } |
---|
441 | |
---|
442 | // Also search in template name |
---|
443 | def condition = extendWhereClause( "( %s )", objectToSearchIn + ".template.name", prefix, "String", castValue( "String" ) ); |
---|
444 | whereClause += condition[ "where" ]; |
---|
445 | |
---|
446 | condition[ "parameters" ].each { |
---|
447 | parameters[ it.key ] = it.value; |
---|
448 | } |
---|
449 | |
---|
450 | return [ "where": whereClause, "parameters": parameters] |
---|
451 | } |
---|
452 | |
---|
453 | /** |
---|
454 | * Extends a given condition with a where clause of this criterion. If you supply "select * from Study where %s", %s will |
---|
455 | * be replaced by the where clause for the given field. Also, the parameters map will be extended (if needed) |
---|
456 | * |
---|
457 | * @param hql Initial HQL string where the clause will be put into |
---|
458 | * @param fieldName Name of the field that should be referenced |
---|
459 | * @param uniquePrefix Unique prefix for this criterion |
---|
460 | * @param fieldType Type of field value to search for |
---|
461 | * @param fieldValue Field value to search for |
---|
462 | * @return Map with 'where' key referencing the extended where clause and 'parameters' key referencing a map with parameters. |
---|
463 | */ |
---|
464 | protected Map extendWhereClause( String hql, String fieldName, String uniquePrefix, String fieldType, def fieldValue ) { |
---|
465 | def parameters = [:] |
---|
466 | def textFieldTypes = [ 'String', 'Text', 'File', 'StringList', 'ExtendableStringList', 'Term', 'Template', 'Module' ]; |
---|
467 | |
---|
468 | switch( this.operator ) { |
---|
469 | case Operator.contains: |
---|
470 | // Text fields should be handled case insensitive |
---|
471 | if( textFieldTypes.contains( fieldType ) ) { |
---|
472 | hql = sprintf( hql, "lower( " + fieldName + ") like lower( :" + uniquePrefix + "ValueLike )" ); |
---|
473 | } else { |
---|
474 | hql = sprintf( hql, fieldName + " like :" + uniquePrefix + "ValueLike" ); |
---|
475 | } |
---|
476 | parameters[ uniquePrefix + "ValueLike" ] = "%" + fieldValue + "%" |
---|
477 | break; |
---|
478 | case Operator.equals: |
---|
479 | case Operator.gte: |
---|
480 | case Operator.gt: |
---|
481 | case Operator.lte: |
---|
482 | case Operator.lt: |
---|
483 | if( textFieldTypes.contains( fieldType ) ) { |
---|
484 | hql = sprintf( hql, "lower( " + fieldName + " ) " + this.operator.name + " lower( :" + uniquePrefix + "Value" + fieldType + ")" ); |
---|
485 | } else { |
---|
486 | hql = sprintf( hql, fieldName + " " + this.operator.name + " :" + uniquePrefix + "Value" + fieldType ); |
---|
487 | } |
---|
488 | parameters[ uniquePrefix + "Value" + fieldType ] = fieldValue |
---|
489 | break; |
---|
490 | } |
---|
491 | |
---|
492 | return [ "where": hql, "parameters": parameters] |
---|
493 | } |
---|
494 | |
---|
495 | /** |
---|
496 | * Creates a condition for this criterion, for a given fieldName and value. The fieldName should reference a collection that has a one-to-many |
---|
497 | * relation with the object being sought |
---|
498 | * |
---|
499 | * @param fieldName Name to search in |
---|
500 | * @param uniquePrefix Unique prefix for this criterion |
---|
501 | * @param currentValue Map with 'value' referencing the value being sought and 'type' referencing |
---|
502 | * the type of the value as string. The value should be be casted to the right class for this field. |
---|
503 | * @return Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters. |
---|
504 | */ |
---|
505 | protected Map oneToManyWhereCondition( String fieldName, String uniquePrefix, String fieldType, def fieldValue ) { |
---|
506 | // Create the where condition for checking the value |
---|
507 | // First check the name of the field, if needed |
---|
508 | def condition |
---|
509 | def parameters = [:] |
---|
510 | |
---|
511 | if( this.field != '*' ) { |
---|
512 | condition = "( %s AND index(" + fieldName + ") = :" + uniquePrefix + "Field )" |
---|
513 | parameters[ uniquePrefix + "Field" ] = this.field |
---|
514 | } else { |
---|
515 | condition = "%s"; |
---|
516 | } |
---|
517 | |
---|
518 | def whereClause = extendWhereClause( condition, fieldName, uniquePrefix, fieldType, fieldValue ); |
---|
519 | parameters.each { |
---|
520 | whereClause.parameters[ it.key ] = it.value; |
---|
521 | } |
---|
522 | |
---|
523 | return whereClause; |
---|
524 | } |
---|
525 | |
---|
526 | /** |
---|
527 | * Creates a condition for this criterion, for a given fieldName and value. The fieldName should |
---|
528 | * reference a collection that has a many-to-many relation with the object being sought (e.g. templateTermFields). |
---|
529 | * |
---|
530 | * Unfortunately, there is no way to determine the name of the field in HQL for this many-to-many collections, since the |
---|
531 | * INDEX() function in HQL doesn't work for many-to-many collections. |
---|
532 | * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4879 |
---|
533 | * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 |
---|
534 | * |
---|
535 | * @param fieldName Name to search in |
---|
536 | * @param uniquePrefix Unique prefix for this criterion |
---|
537 | * @param currentValue Map with 'value' referencing the value being sought and 'type' referencing |
---|
538 | * the type of the value as string. The value should be be casted to the right class for this field. |
---|
539 | * @return Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters. |
---|
540 | */ |
---|
541 | protected Map manyToManyWhereCondition( String objectToSearchIn, String collection, String uniquePrefix, String searchField, def value ) { |
---|
542 | // exists( FROM [objectToSearchIn].[collection] as [uniquePrefix][collection] WHERE [searchField] LIKE [value] ) |
---|
543 | // Create the where condition for checking the value |
---|
544 | def condition = "exists ( FROM " + objectToSearchIn + "." + collection + " as " + uniquePrefix + "_" + collection + " WHERE %s )"; |
---|
545 | |
---|
546 | return extendWhereClause( condition, uniquePrefix + "_" + collection + "." + searchField, uniquePrefix, "String", value ); |
---|
547 | } |
---|
548 | |
---|
549 | /** |
---|
550 | * Retrieves the correct value for this criterion in the given object (with template) |
---|
551 | * |
---|
552 | * @param entity Entity to check for value. Should be a child of template entity |
---|
553 | * @param criterion Criterion to match on |
---|
554 | * @return Value of the given field or null if the field doesn't exist |
---|
555 | */ |
---|
556 | public def getFieldValue( TemplateEntity entity ) { |
---|
557 | if( entity == null ) |
---|
558 | return null; |
---|
559 | |
---|
560 | try { |
---|
561 | def fieldValue |
---|
562 | if( !field ) { |
---|
563 | fieldValue = entity |
---|
564 | } else if( field == "Template" ) { |
---|
565 | fieldValue = entity.template?.name |
---|
566 | } else if( field == "*" ) { |
---|
567 | fieldValue = entity.giveFields().collect{ |
---|
568 | if( it && it.name ) { |
---|
569 | Search.prepare( entity.getFieldValue( it.name ), entity.giveFieldType( it.name ) ) |
---|
570 | } |
---|
571 | } |
---|
572 | } else { |
---|
573 | fieldValue = Search.prepare( entity.getFieldValue( field ), entity.giveFieldType( field ) ) |
---|
574 | } |
---|
575 | |
---|
576 | return fieldValue |
---|
577 | } catch( Exception e ) { |
---|
578 | // An exception occurs if the given field doesn't exist. In that case, this criterion will fail. |
---|
579 | // TODO: Maybe give the user a choice whether he want's to include these studies or not |
---|
580 | return null; |
---|
581 | } |
---|
582 | } |
---|
583 | |
---|
584 | /** |
---|
585 | * Tries to match a value against a criterion and returns true if it matches |
---|
586 | * |
---|
587 | * @param value Value of the field to match |
---|
588 | * @return True iff the value matches this criterion, false otherwise |
---|
589 | */ |
---|
590 | public boolean match( def fieldValue ) { |
---|
591 | if( fieldValue == null ) |
---|
592 | return false; |
---|
593 | |
---|
594 | // in-search criteria have to be handled separately |
---|
595 | if( this.operator == Operator.insearch ) { |
---|
596 | return this.value?.getResults()?.contains( fieldValue ); |
---|
597 | } |
---|
598 | |
---|
599 | // Other criteria are handled based on the class of the value given. |
---|
600 | def classname = fieldValue.class.getName(); |
---|
601 | classname = classname[classname.lastIndexOf( '.' ) + 1..-1].toLowerCase(); |
---|
602 | |
---|
603 | def matches = false; |
---|
604 | try { |
---|
605 | switch( classname ) { |
---|
606 | case "integer": matches = longCompare( new Long( fieldValue.longValue() ) ); break; |
---|
607 | case "long": matches = longCompare( fieldValue ); break; |
---|
608 | case "float": matches = doubleCompare( new Long( fieldValue.doubleValue() ) ); break; |
---|
609 | case "double": matches = doubleCompare( fieldValue ); break; |
---|
610 | case "boolean": matches = booleanCompare( fieldValue ); break; |
---|
611 | case "date": matches = dateCompare( fieldValue); break; |
---|
612 | case "reltime": matches = relTimeCompare( fieldValue ); break; |
---|
613 | case "assaymodule": |
---|
614 | case "template": |
---|
615 | case "term": |
---|
616 | case "templatefieldlistitem": |
---|
617 | case "string": |
---|
618 | default: matches = compareValues( fieldValue.toString().trim().toLowerCase(), this.operator, value.toString().toLowerCase().trim() ); break; |
---|
619 | } |
---|
620 | |
---|
621 | return matches; |
---|
622 | } catch( Exception e ) { |
---|
623 | log.error e.class.getName() + ": " + e.getMessage(); |
---|
624 | return false; |
---|
625 | } |
---|
626 | } |
---|
627 | |
---|
628 | /** |
---|
629 | * Tries to match a value against a criterion and returns true if it matches |
---|
630 | * |
---|
631 | * @param fieldValue Value of the field to match |
---|
632 | * @param operator Operator to apply |
---|
633 | * @param criterionValue Value of the criterion |
---|
634 | * @return True iff the value matches this criterion value, false otherwise |
---|
635 | */ |
---|
636 | protected boolean compareValues( def fieldValue, Operator operator, def criterionValue ) { |
---|
637 | switch( operator ) { |
---|
638 | case Operator.gte: |
---|
639 | return fieldValue >= criterionValue; |
---|
640 | case Operator.gt: |
---|
641 | return fieldValue > criterionValue; |
---|
642 | case Operator.lt: |
---|
643 | return fieldValue < criterionValue; |
---|
644 | case Operator.lte: |
---|
645 | return fieldValue <= criterionValue; |
---|
646 | case Operator.contains: |
---|
647 | // Contains operator can only be used on string values |
---|
648 | return fieldValue.toString().contains( criterionValue.toString() ); |
---|
649 | case Operator.equals: |
---|
650 | default: |
---|
651 | return fieldValue.equals( criterionValue ); |
---|
652 | } |
---|
653 | |
---|
654 | } |
---|
655 | |
---|
656 | /** |
---|
657 | * Tries to match a date value against a criterion and returns true if it matches |
---|
658 | * |
---|
659 | * @param value Date value of the field to match |
---|
660 | * @return True iff the value matches this criterion, false otherwise |
---|
661 | */ |
---|
662 | protected boolean dateCompare( Date fieldValue ) { |
---|
663 | try { |
---|
664 | Date dateCriterion = new SimpleDateFormat( "yyyy-MM-dd" ).parse( value ); |
---|
665 | Date fieldDate = new Date( fieldValue.getTime() ); |
---|
666 | |
---|
667 | // Clear time in order to just compare dates |
---|
668 | dateCriterion.clearTime(); |
---|
669 | fieldDate.clearTime(); |
---|
670 | |
---|
671 | return compareValues( fieldDate, this.operator, dateCriterion ) |
---|
672 | } catch( Exception e ) { |
---|
673 | log.error e.class.getName() + ": " + e.getMessage(); |
---|
674 | return false; |
---|
675 | } |
---|
676 | } |
---|
677 | |
---|
678 | /** |
---|
679 | * Tries to match a long value against a criterion and returns true if it matches |
---|
680 | * |
---|
681 | * @param value Long value of the field to match |
---|
682 | * @param criterion Criterion to match on. Should be a map with entries 'operator' and 'value' |
---|
683 | * @return True iff the value matches this criterion, false otherwise |
---|
684 | */ |
---|
685 | protected boolean longCompare( Long fieldValue ) { |
---|
686 | Long longCriterion; |
---|
687 | try { |
---|
688 | longCriterion = Long.parseLong( value ); |
---|
689 | } catch( Exception e ) { |
---|
690 | try { |
---|
691 | // If converting to long doesn't work, try converting to double and rounding it |
---|
692 | Double doubleCriterion = Double.parseDouble(value); |
---|
693 | longCriterion = new Long( doubleCriterion.longValue() ); |
---|
694 | } catch( Exception e2 ) { |
---|
695 | log.debug "Can't convert value to long for comparison: " + e2.class.getName() + ": " + e2.getMessage(); |
---|
696 | return false; |
---|
697 | } |
---|
698 | } |
---|
699 | return compareValues( fieldValue, this.operator, longCriterion ); |
---|
700 | } |
---|
701 | |
---|
702 | /** |
---|
703 | * Tries to match a double value against a criterion and returns true if it matches |
---|
704 | * |
---|
705 | * @param value Double value of the field to match |
---|
706 | * @return True iff the value matches this criterion, false otherwise |
---|
707 | */ |
---|
708 | protected boolean doubleCompare( Double fieldValue ) { |
---|
709 | try { |
---|
710 | Double doubleCriterion = Double.parseDouble( value ); |
---|
711 | return compareValues( fieldValue, this.operator, doubleCriterion ); |
---|
712 | } catch( Exception e ) { |
---|
713 | log.debug "Can't convert value to double for comparison: " + e.class.getName() + ": " + e.getMessage(); |
---|
714 | return false; |
---|
715 | } |
---|
716 | } |
---|
717 | |
---|
718 | |
---|
719 | /** |
---|
720 | * Tries to match a boolean value against a criterion and returns true if it matches |
---|
721 | * |
---|
722 | * @param value Boolean value of the field to match |
---|
723 | * @return True iff the value matches this criterion, false otherwise |
---|
724 | */ |
---|
725 | protected boolean booleanCompare( Boolean fieldValue ) { |
---|
726 | try { |
---|
727 | // The comparison should only be performed iff the value |
---|
728 | // contains 'true' or 'false' (case insensitive) |
---|
729 | def lowerCaseValue = value.toString().toLowerCase(); |
---|
730 | if( lowerCaseValue != 'true' && lowerCaseValue != 'false' ) |
---|
731 | return false; |
---|
732 | |
---|
733 | Boolean booleanCriterion = Boolean.parseBoolean( value ); |
---|
734 | return compareValues( fieldValue, this.operator, booleanCriterion ); |
---|
735 | } catch( Exception e ) { |
---|
736 | log.debug "Can't convert value to boolean for comparison: " + e.class.getName() + ": " + e.getMessage(); |
---|
737 | return false; |
---|
738 | } |
---|
739 | } |
---|
740 | |
---|
741 | /** |
---|
742 | * Tries to match a relTime value against a criterion and returns true if it matches |
---|
743 | * |
---|
744 | * @param value relTime value of the field to match |
---|
745 | * @return True iff the value matches this criterion, false otherwise |
---|
746 | */ |
---|
747 | protected boolean relTimeCompare( RelTime fieldValue ) { |
---|
748 | try { |
---|
749 | RelTime rt |
---|
750 | |
---|
751 | // Numbers are taken to be seconds, if a non-numeric value is given, try to parse it |
---|
752 | if( value.toString().isLong() ) { |
---|
753 | rt = new RelTime( Long.parseLong( value.toString() ) ); |
---|
754 | } else { |
---|
755 | rt = new RelTime( value.toString() ); |
---|
756 | } |
---|
757 | |
---|
758 | return compareValues( fieldValue, this.operator, rt ); |
---|
759 | } catch( Exception e ) { |
---|
760 | log.debug "Can't convert value to reltime for comparison: " + e.class.getName() + ": " + e.getMessage(); |
---|
761 | return false; |
---|
762 | } |
---|
763 | } |
---|
764 | |
---|
765 | public static Operator parseOperator( String name ) throws Exception { |
---|
766 | switch( name.trim() ) { |
---|
767 | case "=": |
---|
768 | case "equals": return Operator.equals; |
---|
769 | case "contains": return Operator.contains; |
---|
770 | case ">=": |
---|
771 | case "gte": return Operator.gte; |
---|
772 | case ">": |
---|
773 | case "gt": return Operator.gt; |
---|
774 | case "<=": |
---|
775 | case "lte": return Operator.lte; |
---|
776 | case "<": |
---|
777 | case "lt": return Operator.lt; |
---|
778 | case "in": return Operator.insearch; |
---|
779 | default: |
---|
780 | throw new Exception( "Operator not found" ); |
---|
781 | } |
---|
782 | } |
---|
783 | |
---|
784 | public String toString() { |
---|
785 | return "[Criterion " + entityField() + " " + operator + " " + value + "]"; |
---|
786 | } |
---|
787 | |
---|
788 | public boolean equals( Object o ) { |
---|
789 | if( o == null ) |
---|
790 | return false; |
---|
791 | |
---|
792 | if( !( o instanceof Criterion ) ) |
---|
793 | return false; |
---|
794 | |
---|
795 | Criterion otherCriterion = (Criterion) o; |
---|
796 | return this.entity == otherCriterion.entity && |
---|
797 | this.field == otherCriterion.field && |
---|
798 | this.operator == otherCriterion.operator && |
---|
799 | this.value == otherCriterion.value; |
---|
800 | } |
---|
801 | } |
---|