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 | case 'File': |
---|
197 | return value?.toString(); |
---|
198 | case 'Date': |
---|
199 | // The comparison with date values should only be performed iff the value |
---|
200 | // contains a parsable date |
---|
201 | // and the operator is equals, gte, gt, lt or lte |
---|
202 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
203 | return null |
---|
204 | |
---|
205 | try { |
---|
206 | Date dateCriterion = new SimpleDateFormat( "yyyy-MM-dd" ).parse( value ); |
---|
207 | return dateCriterion |
---|
208 | } catch( Exception e ) { |
---|
209 | return null; |
---|
210 | } |
---|
211 | |
---|
212 | case 'RelTime': |
---|
213 | // The comparison with date values should only be performed iff the value |
---|
214 | // contains a long number |
---|
215 | // and the operator is equals, gte, gt, lt or lte |
---|
216 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
217 | return null |
---|
218 | |
---|
219 | try { |
---|
220 | RelTime rt |
---|
221 | |
---|
222 | // Numbers are taken to be seconds, if a non-numeric value is given, try to parse it |
---|
223 | if( value.toString().isLong() ) { |
---|
224 | rt = new RelTime( Long.parseLong( value.toString() ) ); |
---|
225 | } else { |
---|
226 | rt = new RelTime( value.toString() ); |
---|
227 | } |
---|
228 | |
---|
229 | return rt.getValue() |
---|
230 | } catch( Exception e ) { |
---|
231 | return null; |
---|
232 | } |
---|
233 | case 'Double': |
---|
234 | // The comparison with date values should only be performed iff the value |
---|
235 | // contains a double number |
---|
236 | // and the operator is equals, gte, gt, lt or lte |
---|
237 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
238 | return null |
---|
239 | |
---|
240 | if( value.isDouble() ) { |
---|
241 | return Double.parseDouble( value ) |
---|
242 | } else { |
---|
243 | return null; |
---|
244 | } |
---|
245 | case 'Long': |
---|
246 | // The comparison with date values should only be performed iff the value |
---|
247 | // contains a long number |
---|
248 | // and the operator is equals, gte, gt, lt or lte |
---|
249 | if( operator == Operator.insearch || operator == Operator.contains ) |
---|
250 | return null |
---|
251 | |
---|
252 | if( value.isLong() ) { |
---|
253 | return Long.parseLong( value ) |
---|
254 | } else { |
---|
255 | return null; |
---|
256 | } |
---|
257 | case 'Boolean': |
---|
258 | // The comparison with boolean values should only be performed iff the value |
---|
259 | // contains 'true' or 'false' (case insensitive) |
---|
260 | // and the operator is equals |
---|
261 | if( operator != Operator.equals ) |
---|
262 | return null |
---|
263 | |
---|
264 | def lowerCaseValue = value.toString().toLowerCase(); |
---|
265 | if( lowerCaseValue == 'true' || lowerCaseValue == 'false' ) { |
---|
266 | return Boolean.parseBoolean( this.value ) |
---|
267 | } else { |
---|
268 | return null |
---|
269 | } |
---|
270 | } |
---|
271 | } |
---|
272 | |
---|
273 | /** |
---|
274 | * Create a HQL where clause from this criterion, in order to be used within a larger HQL statement |
---|
275 | * |
---|
276 | * @param objectToSearchIn HQL name of the object to search in |
---|
277 | * @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 |
---|
278 | */ |
---|
279 | public Map toHQL( String prefix, String objectToSearchIn = "object" ) { |
---|
280 | List whereClause = [] |
---|
281 | String joinClause = ""; |
---|
282 | Map parameters = [:]; |
---|
283 | def emptyCriterion = [ "join": null, "where": null, "parameters": null ]; |
---|
284 | |
---|
285 | // If this criterion is used to search within another search result, we use a special piece of HQL |
---|
286 | if( this.operator == Operator.insearch ) { |
---|
287 | if( this.value?.results ) { |
---|
288 | parameters[ prefix + "SearchResults" ] = this.value?.results |
---|
289 | |
---|
290 | return [ "join": "", "where": "( " + objectToSearchIn + " in (:" + prefix + "SearchResults) )" , "parameters": parameters ]; |
---|
291 | } else { |
---|
292 | return emptyCriterion; |
---|
293 | } |
---|
294 | } |
---|
295 | |
---|
296 | // If no value is given, don't do anything |
---|
297 | if( value == null ) |
---|
298 | return emptyCriterion; |
---|
299 | |
---|
300 | // Check whether the field is a domain field |
---|
301 | if( isDomainCriterion() ) { |
---|
302 | // Determine the types of this criterion, but there will be only 1 for a domain field |
---|
303 | def criterionType = criterionType()[0]; |
---|
304 | |
---|
305 | // Some domain fields don't contain a value, but a reference to another table |
---|
306 | // These should be handled differently |
---|
307 | def fieldName = this.field |
---|
308 | |
---|
309 | // Make sure the Template field is referenced as lowercase |
---|
310 | if( fieldName == "Template" ) |
---|
311 | fieldName = "template"; |
---|
312 | |
---|
313 | if( |
---|
314 | ( fieldName == "template" ) || |
---|
315 | ( objectToSearchIn.toLowerCase() == "subject" && fieldName.toLowerCase() == "species" ) || |
---|
316 | ( objectToSearchIn.toLowerCase() == "sample" && fieldName.toLowerCase() == "material" ) || |
---|
317 | ( objectToSearchIn.toLowerCase() == "assay" && fieldName.toLowerCase() == "module" ) || |
---|
318 | ( objectToSearchIn.toLowerCase() == "samplingevent" && fieldName.toLowerCase() == "sampletemplate" ) ) { |
---|
319 | fieldName += ".name" |
---|
320 | } |
---|
321 | |
---|
322 | def query = extendWhereClause( "( %s )", objectToSearchIn + "." + fieldName, prefix, criterionType, castValue( criterionType ) ); |
---|
323 | return [ "join": "", "where": query.where, "parameters": query.parameters ] |
---|
324 | } |
---|
325 | |
---|
326 | // Determine the type of this criterion |
---|
327 | def criterionTypes = criterionType(); |
---|
328 | |
---|
329 | if( !criterionTypes ) |
---|
330 | return emptyCriterion; |
---|
331 | |
---|
332 | // Several types of criteria are handled differently. |
---|
333 | // The 'wildcard' is handled by searching for all types. |
---|
334 | // The 'simple' types (string, double) are handled by searching in the associated table |
---|
335 | // The 'complex' types (stringlist, template etc., referencing another |
---|
336 | // database table) can't be handled correctly, since the HQL INDEX() function doesn't work on those relations. |
---|
337 | // We do a search for these types to see whether any field with that type fits this criterion, in order to |
---|
338 | // filter out false positives later on. |
---|
339 | criterionTypes.findAll { it }.each { criterionType -> |
---|
340 | // Cast criterion value to the right type |
---|
341 | def currentValue = castValue( criterionType ); |
---|
342 | |
---|
343 | // Determine field name |
---|
344 | def fieldName = "template" + criterionType + 'Fields' |
---|
345 | |
---|
346 | switch( criterionType ) { |
---|
347 | case "Wildcard": |
---|
348 | // Wildcard search is handled by |
---|
349 | break; |
---|
350 | |
---|
351 | case 'String': |
---|
352 | case 'Text': |
---|
353 | case 'File': |
---|
354 | case 'Date': |
---|
355 | case 'RelTime': |
---|
356 | case 'Double': |
---|
357 | case 'Long': |
---|
358 | case 'Boolean': |
---|
359 | // 'Simple' field types |
---|
360 | if( currentValue != null ) { |
---|
361 | joinClause += " left join " + objectToSearchIn + "." + fieldName + " as " + prefix + "_" + fieldName + " "; |
---|
362 | |
---|
363 | def condition = this.oneToManyWhereCondition( prefix + "_" + fieldName, prefix, criterionType, currentValue ) |
---|
364 | whereClause += condition[ "where" ]; |
---|
365 | |
---|
366 | condition[ "parameters" ].each { |
---|
367 | parameters[ it.key ] = it.value; |
---|
368 | } |
---|
369 | } |
---|
370 | break; |
---|
371 | |
---|
372 | case 'StringList': |
---|
373 | case 'ExtendableStringList': |
---|
374 | case 'Term': |
---|
375 | case 'Template': |
---|
376 | case 'Module': |
---|
377 | // 'Complex' field types |
---|
378 | def condition = this.manyToManyWhereCondition( objectToSearchIn, fieldName, prefix, "name", currentValue ) |
---|
379 | whereClause += condition[ "where" ]; |
---|
380 | |
---|
381 | condition[ "parameters" ].each { |
---|
382 | parameters[ it.key ] = it.value; |
---|
383 | } |
---|
384 | default: |
---|
385 | break; |
---|
386 | } |
---|
387 | } |
---|
388 | |
---|
389 | // Wildcard searches must also search the fixed domain fields of all entities. |
---|
390 | if( this.field == '*' ) { |
---|
391 | def condition = wildcardDomainFields( prefix, objectToSearchIn ) |
---|
392 | |
---|
393 | whereClause += condition[ "where" ]; |
---|
394 | condition[ "parameters" ].each { |
---|
395 | parameters[ it.key ] = it.value; |
---|
396 | } |
---|
397 | } |
---|
398 | |
---|
399 | def where = whereClause?.findAll { it } ? "( " + whereClause.findAll { it }.join( " OR " ) + " )" : "" |
---|
400 | |
---|
401 | return [ "join": joinClause, "where": where , "parameters": parameters ]; |
---|
402 | } |
---|
403 | |
---|
404 | protected Map wildcardDomainFields( String prefix, String objectToSearchIn ) { |
---|
405 | def whereClause = []; |
---|
406 | def parameters = [:]; |
---|
407 | |
---|
408 | // Determine all domain fields |
---|
409 | def domainFields |
---|
410 | |
---|
411 | switch( objectToSearchIn.toLowerCase() ) { |
---|
412 | case "study": domainFields = Study.giveDomainFields(); break; |
---|
413 | case "subject": domainFields = Subject.giveDomainFields(); break; |
---|
414 | case "event": domainFields = Event.giveDomainFields(); break; |
---|
415 | case "sample": domainFields = Sample.giveDomainFields(); break; |
---|
416 | case "assay": domainFields = Assay.giveDomainFields(); break; |
---|
417 | case "samplingevent": domainFields = SamplingEvent.giveDomainFields(); break; |
---|
418 | } |
---|
419 | |
---|
420 | domainFields.each { field -> |
---|
421 | def criterionType = field.type?.casedName; |
---|
422 | |
---|
423 | def fieldName = field.name; |
---|
424 | def condition |
---|
425 | |
---|
426 | // Searching in (complex) domain fields with value NULL will result in the whole |
---|
427 | // where clause to fail. e.g.: |
---|
428 | // |
---|
429 | // SELECT * FROM Sample sample WHERE sample.material LIKE '%m%' OR 1 = 1 |
---|
430 | // should result in all samples. However, it only results in those samples |
---|
431 | // with sample.material =! NULL. |
---|
432 | // |
---|
433 | // For that reason, we use a subquery to select those |
---|
434 | if( ( objectToSearchIn.toLowerCase() == "subject" && fieldName.toLowerCase() == "species" ) || |
---|
435 | ( objectToSearchIn.toLowerCase() == "sample" && fieldName.toLowerCase() == "material" ) || |
---|
436 | ( objectToSearchIn.toLowerCase() == "assay" && fieldName.toLowerCase() == "module" ) || |
---|
437 | ( objectToSearchIn.toLowerCase() == "samplingevent" && fieldName.toLowerCase() == "sampletemplate" ) ) { |
---|
438 | |
---|
439 | condition = manyToManyWhereCondition( objectToSearchIn, fieldName, prefix, "name", value ); |
---|
440 | fieldName += ".name" |
---|
441 | } else { |
---|
442 | condition = extendWhereClause( "( %s )", objectToSearchIn + "." + fieldName, prefix, criterionType, castValue( criterionType ) ); |
---|
443 | } |
---|
444 | |
---|
445 | // Search in fields |
---|
446 | whereClause += condition[ "where" ]; |
---|
447 | |
---|
448 | condition[ "parameters" ].each { |
---|
449 | parameters[ it.key ] = it.value; |
---|
450 | } |
---|
451 | } |
---|
452 | |
---|
453 | // Also search in template name |
---|
454 | def condition = extendWhereClause( "( %s )", objectToSearchIn + ".template.name", prefix, "String", castValue( "String" ) ); |
---|
455 | whereClause += condition[ "where" ]; |
---|
456 | |
---|
457 | condition[ "parameters" ].each { |
---|
458 | parameters[ it.key ] = it.value; |
---|
459 | } |
---|
460 | |
---|
461 | return [ "where": whereClause, "parameters": parameters] |
---|
462 | } |
---|
463 | |
---|
464 | /** |
---|
465 | * Extends a given condition with a where clause of this criterion. If you supply "select * from Study where %s", %s will |
---|
466 | * be replaced by the where clause for the given field. Also, the parameters map will be extended (if needed) |
---|
467 | * |
---|
468 | * @param hql Initial HQL string where the clause will be put into |
---|
469 | * @param fieldName Name of the field that should be referenced |
---|
470 | * @param uniquePrefix Unique prefix for this criterion |
---|
471 | * @param fieldType Type of field value to search for |
---|
472 | * @param fieldValue Field value to search for |
---|
473 | * @return Map with 'where' key referencing the extended where clause and 'parameters' key referencing a map with parameters. |
---|
474 | */ |
---|
475 | protected Map extendWhereClause( String hql, String fieldName, String uniquePrefix, String fieldType, def fieldValue ) { |
---|
476 | def parameters = [:] |
---|
477 | def textFieldTypes = [ 'String', 'Text', 'File', 'StringList', 'ExtendableStringList', 'Term', 'Template', 'Module' ]; |
---|
478 | |
---|
479 | switch( this.operator ) { |
---|
480 | case Operator.contains: |
---|
481 | // Every field contains an empty string, so we don't search on it |
---|
482 | if( !fieldValue ) |
---|
483 | return [ "where": "", "parameters": [:] ] |
---|
484 | |
---|
485 | // Text fields should be handled case insensitive |
---|
486 | def clause |
---|
487 | if( textFieldTypes.contains( fieldType ) ) { |
---|
488 | clause = "lower( " + fieldName + ") like lower( :" + uniquePrefix + "ValueLike )" |
---|
489 | } else { |
---|
490 | clause = fieldName + " like :" + uniquePrefix + "ValueLike"; |
---|
491 | } |
---|
492 | |
---|
493 | hql = sprintf( hql, clause ); |
---|
494 | parameters[ uniquePrefix + "ValueLike" ] = "%" + fieldValue + "%" |
---|
495 | break; |
---|
496 | case Operator.equals: |
---|
497 | case Operator.gte: |
---|
498 | case Operator.gt: |
---|
499 | case Operator.lte: |
---|
500 | case Operator.lt: |
---|
501 | // Text fields should be handled case insensitive |
---|
502 | def clause; |
---|
503 | if( textFieldTypes.contains( fieldType ) ) { |
---|
504 | clause = "lower( " + fieldName + " ) " + this.operator.name + " lower( :" + uniquePrefix + "Value" + fieldType + ")" |
---|
505 | } else { |
---|
506 | clause = fieldName + " " + this.operator.name + " :" + uniquePrefix + "Value" + fieldType |
---|
507 | } |
---|
508 | |
---|
509 | // If the user searches for an empty string, it should also match the database NULL value |
---|
510 | if( operator == Operator.equals && !fieldValue ) { |
---|
511 | clause += " OR " + fieldName + " IS NULL" |
---|
512 | } |
---|
513 | |
---|
514 | hql = sprintf( hql, clause ); |
---|
515 | parameters[ uniquePrefix + "Value" + fieldType ] = fieldValue |
---|
516 | break; |
---|
517 | } |
---|
518 | |
---|
519 | return [ "where": hql, "parameters": parameters] |
---|
520 | } |
---|
521 | |
---|
522 | /** |
---|
523 | * Creates a condition for this criterion, for a given fieldName and value. The fieldName should reference a collection that has a one-to-many |
---|
524 | * relation with the object being sought |
---|
525 | * |
---|
526 | * @param fieldName Name to search in |
---|
527 | * @param uniquePrefix Unique prefix for this criterion |
---|
528 | * @param currentValue Map with 'value' referencing the value being sought and 'type' referencing |
---|
529 | * the type of the value as string. The value should be be casted to the right class for this field. |
---|
530 | * @return Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters. |
---|
531 | */ |
---|
532 | protected Map oneToManyWhereCondition( String fieldName, String uniquePrefix, String fieldType, def fieldValue ) { |
---|
533 | // Create the where condition for checking the value |
---|
534 | // First check the name of the field, if needed |
---|
535 | def condition |
---|
536 | def parameters = [:] |
---|
537 | |
---|
538 | // If looking for a specific field, that field should also be mentioned in the where clause |
---|
539 | if( this.field != '*' ) { |
---|
540 | condition = "( %s AND index(" + fieldName + ") = :" + uniquePrefix + "Field )" |
---|
541 | parameters[ uniquePrefix + "Field" ] = this.field |
---|
542 | } else { |
---|
543 | condition = "%s"; |
---|
544 | } |
---|
545 | |
---|
546 | def whereClause = extendWhereClause( condition, fieldName, uniquePrefix, fieldType, fieldValue ); |
---|
547 | parameters.each { |
---|
548 | whereClause.parameters[ it.key ] = it.value; |
---|
549 | } |
---|
550 | |
---|
551 | return whereClause; |
---|
552 | } |
---|
553 | |
---|
554 | /** |
---|
555 | * Creates a condition for this criterion, for a given fieldName and value. The fieldName should |
---|
556 | * reference a collection that has a many-to-many relation with the object being sought (e.g. templateTermFields). |
---|
557 | * |
---|
558 | * Unfortunately, there is no way to determine the name of the field in HQL for this many-to-many collections, since the |
---|
559 | * INDEX() function in HQL doesn't work for many-to-many collections. |
---|
560 | * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4879 |
---|
561 | * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 |
---|
562 | * |
---|
563 | * @param fieldName Name to search in |
---|
564 | * @param uniquePrefix Unique prefix for this criterion |
---|
565 | * @param currentValue Map with 'value' referencing the value being sought and 'type' referencing |
---|
566 | * the type of the value as string. The value should be be casted to the right class for this field. |
---|
567 | * @return Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters. |
---|
568 | */ |
---|
569 | protected Map manyToManyWhereCondition( String objectToSearchIn, String collection, String uniquePrefix, String searchField, def value ) { |
---|
570 | // exists( FROM [objectToSearchIn].[collection] as [uniquePrefix][collection] WHERE [searchField] LIKE [value] ) |
---|
571 | // Create the where condition for checking the value |
---|
572 | def condition = "exists ( FROM " + objectToSearchIn + "." + collection + " as " + uniquePrefix + "_" + collection + " WHERE %s )"; |
---|
573 | |
---|
574 | return extendWhereClause( condition, uniquePrefix + "_" + collection + "." + searchField, uniquePrefix, "String", value ); |
---|
575 | } |
---|
576 | |
---|
577 | /** |
---|
578 | * Retrieves the correct value for this criterion in the given object (with template) |
---|
579 | * |
---|
580 | * @param entity Entity to check for value. Should be a child of template entity |
---|
581 | * @param criterion Criterion to match on |
---|
582 | * @return Value of the given field or null if the field doesn't exist |
---|
583 | */ |
---|
584 | public def getFieldValue( TemplateEntity entity ) { |
---|
585 | if( entity == null ) |
---|
586 | return null; |
---|
587 | |
---|
588 | try { |
---|
589 | def fieldValue |
---|
590 | if( !field ) { |
---|
591 | fieldValue = entity |
---|
592 | } else if( field == "Template" ) { |
---|
593 | fieldValue = entity.template?.name |
---|
594 | } else if( field == "*" ) { |
---|
595 | fieldValue = entity.giveFields().collect{ |
---|
596 | if( it && it.name ) { |
---|
597 | Search.prepare( entity.getFieldValue( it.name ), entity.giveFieldType( it.name ) ) |
---|
598 | } |
---|
599 | } |
---|
600 | } else { |
---|
601 | fieldValue = Search.prepare( entity.getFieldValue( field ), entity.giveFieldType( field ) ) |
---|
602 | } |
---|
603 | |
---|
604 | return fieldValue |
---|
605 | } catch( Exception e ) { |
---|
606 | // An exception occurs if the given field doesn't exist. In that case, this criterion will fail. |
---|
607 | // TODO: Maybe give the user a choice whether he want's to include these studies or not |
---|
608 | return null; |
---|
609 | } |
---|
610 | } |
---|
611 | |
---|
612 | /** |
---|
613 | * Tries to match a value against a criterion and returns true if it matches |
---|
614 | * |
---|
615 | * @param value Value of the field to match |
---|
616 | * @return True iff the value matches this criterion, false otherwise |
---|
617 | */ |
---|
618 | public boolean match( def fieldValue ) { |
---|
619 | if( fieldValue == null ) |
---|
620 | return false; |
---|
621 | |
---|
622 | // in-search criteria have to be handled separately |
---|
623 | if( this.operator == Operator.insearch ) { |
---|
624 | return this.value?.getResults()?.contains( fieldValue ); |
---|
625 | } |
---|
626 | |
---|
627 | // Other criteria are handled based on the class of the value given. |
---|
628 | def classname = fieldValue.class.getName(); |
---|
629 | classname = classname[classname.lastIndexOf( '.' ) + 1..-1].toLowerCase(); |
---|
630 | |
---|
631 | def matches = false; |
---|
632 | try { |
---|
633 | switch( classname ) { |
---|
634 | case "integer": matches = longCompare( new Long( fieldValue.longValue() ) ); break; |
---|
635 | case "long": matches = longCompare( fieldValue ); break; |
---|
636 | case "float": matches = doubleCompare( new Long( fieldValue.doubleValue() ) ); break; |
---|
637 | case "double": matches = doubleCompare( fieldValue ); break; |
---|
638 | case "boolean": matches = booleanCompare( fieldValue ); break; |
---|
639 | case "date": matches = dateCompare( fieldValue); break; |
---|
640 | case "reltime": matches = relTimeCompare( fieldValue ); break; |
---|
641 | case "assaymodule": |
---|
642 | case "template": |
---|
643 | case "term": |
---|
644 | case "templatefieldlistitem": |
---|
645 | case "string": |
---|
646 | default: matches = compareValues( fieldValue.toString().trim().toLowerCase(), this.operator, value.toString().toLowerCase().trim() ); break; |
---|
647 | } |
---|
648 | |
---|
649 | return matches; |
---|
650 | } catch( Exception e ) { |
---|
651 | log.error e.class.getName() + ": " + e.getMessage(); |
---|
652 | return false; |
---|
653 | } |
---|
654 | } |
---|
655 | |
---|
656 | /** |
---|
657 | * Tries to match a value against a criterion and returns true if it matches |
---|
658 | * |
---|
659 | * @param fieldValue Value of the field to match |
---|
660 | * @param operator Operator to apply |
---|
661 | * @param criterionValue Value of the criterion |
---|
662 | * @return True iff the value matches this criterion value, false otherwise |
---|
663 | */ |
---|
664 | protected boolean compareValues( def fieldValue, Operator operator, def criterionValue ) { |
---|
665 | switch( operator ) { |
---|
666 | case Operator.gte: |
---|
667 | return fieldValue >= criterionValue; |
---|
668 | case Operator.gt: |
---|
669 | return fieldValue > criterionValue; |
---|
670 | case Operator.lt: |
---|
671 | return fieldValue < criterionValue; |
---|
672 | case Operator.lte: |
---|
673 | return fieldValue <= criterionValue; |
---|
674 | case Operator.contains: |
---|
675 | // Contains operator can only be used on string values |
---|
676 | return fieldValue.toString().contains( criterionValue.toString() ); |
---|
677 | case Operator.equals: |
---|
678 | default: |
---|
679 | return fieldValue.equals( criterionValue ); |
---|
680 | } |
---|
681 | |
---|
682 | } |
---|
683 | |
---|
684 | /** |
---|
685 | * Tries to match a date value against a criterion and returns true if it matches |
---|
686 | * |
---|
687 | * @param value Date value of the field to match |
---|
688 | * @return True iff the value matches this criterion, false otherwise |
---|
689 | */ |
---|
690 | protected boolean dateCompare( Date fieldValue ) { |
---|
691 | try { |
---|
692 | Date dateCriterion = new SimpleDateFormat( "yyyy-MM-dd" ).parse( value ); |
---|
693 | Date fieldDate = new Date( fieldValue.getTime() ); |
---|
694 | |
---|
695 | // Clear time in order to just compare dates |
---|
696 | dateCriterion.clearTime(); |
---|
697 | fieldDate.clearTime(); |
---|
698 | |
---|
699 | return compareValues( fieldDate, this.operator, dateCriterion ) |
---|
700 | } catch( Exception e ) { |
---|
701 | log.error e.class.getName() + ": " + e.getMessage(); |
---|
702 | return false; |
---|
703 | } |
---|
704 | } |
---|
705 | |
---|
706 | /** |
---|
707 | * Tries to match a long value against a criterion and returns true if it matches |
---|
708 | * |
---|
709 | * @param value Long value of the field to match |
---|
710 | * @param criterion Criterion to match on. Should be a map with entries 'operator' and 'value' |
---|
711 | * @return True iff the value matches this criterion, false otherwise |
---|
712 | */ |
---|
713 | protected boolean longCompare( Long fieldValue ) { |
---|
714 | Long longCriterion; |
---|
715 | try { |
---|
716 | longCriterion = Long.parseLong( value ); |
---|
717 | } catch( Exception e ) { |
---|
718 | try { |
---|
719 | // If converting to long doesn't work, try converting to double and rounding it |
---|
720 | Double doubleCriterion = Double.parseDouble(value); |
---|
721 | longCriterion = new Long( doubleCriterion.longValue() ); |
---|
722 | } catch( Exception e2 ) { |
---|
723 | log.debug "Can't convert value to long for comparison: " + e2.class.getName() + ": " + e2.getMessage(); |
---|
724 | return false; |
---|
725 | } |
---|
726 | } |
---|
727 | return compareValues( fieldValue, this.operator, longCriterion ); |
---|
728 | } |
---|
729 | |
---|
730 | /** |
---|
731 | * Tries to match a double value against a criterion and returns true if it matches |
---|
732 | * |
---|
733 | * @param value Double value of the field to match |
---|
734 | * @return True iff the value matches this criterion, false otherwise |
---|
735 | */ |
---|
736 | protected boolean doubleCompare( Double fieldValue ) { |
---|
737 | try { |
---|
738 | Double doubleCriterion = Double.parseDouble( value ); |
---|
739 | return compareValues( fieldValue, this.operator, doubleCriterion ); |
---|
740 | } catch( Exception e ) { |
---|
741 | log.debug "Can't convert value to double for comparison: " + e.class.getName() + ": " + e.getMessage(); |
---|
742 | return false; |
---|
743 | } |
---|
744 | } |
---|
745 | |
---|
746 | |
---|
747 | /** |
---|
748 | * Tries to match a boolean value against a criterion and returns true if it matches |
---|
749 | * |
---|
750 | * @param value Boolean value of the field to match |
---|
751 | * @return True iff the value matches this criterion, false otherwise |
---|
752 | */ |
---|
753 | protected boolean booleanCompare( Boolean fieldValue ) { |
---|
754 | try { |
---|
755 | // The comparison should only be performed iff the value |
---|
756 | // contains 'true' or 'false' (case insensitive) |
---|
757 | def lowerCaseValue = value.toString().toLowerCase(); |
---|
758 | if( lowerCaseValue != 'true' && lowerCaseValue != 'false' ) |
---|
759 | return false; |
---|
760 | |
---|
761 | Boolean booleanCriterion = Boolean.parseBoolean( value ); |
---|
762 | return compareValues( fieldValue, this.operator, booleanCriterion ); |
---|
763 | } catch( Exception e ) { |
---|
764 | log.debug "Can't convert value to boolean for comparison: " + e.class.getName() + ": " + e.getMessage(); |
---|
765 | return false; |
---|
766 | } |
---|
767 | } |
---|
768 | |
---|
769 | /** |
---|
770 | * Tries to match a relTime value against a criterion and returns true if it matches |
---|
771 | * |
---|
772 | * @param value relTime value of the field to match |
---|
773 | * @return True iff the value matches this criterion, false otherwise |
---|
774 | */ |
---|
775 | protected boolean relTimeCompare( RelTime fieldValue ) { |
---|
776 | try { |
---|
777 | RelTime rt |
---|
778 | |
---|
779 | // Numbers are taken to be seconds, if a non-numeric value is given, try to parse it |
---|
780 | if( value.toString().isLong() ) { |
---|
781 | rt = new RelTime( Long.parseLong( value.toString() ) ); |
---|
782 | } else { |
---|
783 | rt = new RelTime( value.toString() ); |
---|
784 | } |
---|
785 | |
---|
786 | return compareValues( fieldValue, this.operator, rt ); |
---|
787 | } catch( Exception e ) { |
---|
788 | log.debug "Can't convert value to reltime for comparison: " + e.class.getName() + ": " + e.getMessage(); |
---|
789 | return false; |
---|
790 | } |
---|
791 | } |
---|
792 | |
---|
793 | public static Operator parseOperator( String name ) throws Exception { |
---|
794 | switch( name.trim() ) { |
---|
795 | case "=": |
---|
796 | case "equals": return Operator.equals; |
---|
797 | case "contains": return Operator.contains; |
---|
798 | case ">=": |
---|
799 | case "gte": return Operator.gte; |
---|
800 | case ">": |
---|
801 | case "gt": return Operator.gt; |
---|
802 | case "<=": |
---|
803 | case "lte": return Operator.lte; |
---|
804 | case "<": |
---|
805 | case "lt": return Operator.lt; |
---|
806 | case "in": return Operator.insearch; |
---|
807 | default: |
---|
808 | throw new Exception( "Operator not found" ); |
---|
809 | } |
---|
810 | } |
---|
811 | |
---|
812 | public String toString() { |
---|
813 | return "[Criterion " + entityField() + " " + operator + " " + value + "]"; |
---|
814 | } |
---|
815 | |
---|
816 | public boolean equals( Object o ) { |
---|
817 | if( o == null ) |
---|
818 | return false; |
---|
819 | |
---|
820 | if( !( o instanceof Criterion ) ) |
---|
821 | return false; |
---|
822 | |
---|
823 | Criterion otherCriterion = (Criterion) o; |
---|
824 | return this.entity == otherCriterion.entity && |
---|
825 | this.field == otherCriterion.field && |
---|
826 | this.operator == otherCriterion.operator && |
---|
827 | this.value == otherCriterion.value; |
---|
828 | } |
---|
829 | } |
---|