Changeset 1820
- Timestamp:
- May 6, 2011, 5:41:49 PM (12 years ago)
- Location:
- trunk/src/groovy/dbnp/query
- Files:
-
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/groovy/dbnp/query/AssaySearch.groovy
r1526 r1820 30 30 31 31 this.entity = "Assay"; 32 }33 34 /**35 * Searches for assays based on the given criteria. All criteria have to be satisfied and36 * criteria for the different entities are satisfied as follows:37 *38 * Study.title = 'abc'39 * Only assays are returned from studies with title 'abc'40 *41 * Subject.species = 'human'42 * Only assays are returned with samples from subjects with species = 'human'43 *44 * Sample.name = 'sample 1'45 * Only assays are returned with samples with name = 'sample 1'46 *47 * Event.startTime = '0s'48 * Only assays are returned with samples from subjects that have had an event with start time = '0s'49 *50 * SamplingEvent.startTime = '0s'51 * Only assays are returned with samples that have originated from a sampling event with start time = '0s'52 *53 * Assay.module = 'metagenomics'54 * Only assays are returned with module = metagenomics55 *56 * When searching for more than one criterion per entity, these are taken combined. Searching for57 *58 * Subject.species = 'human'59 * Subject.name = 'Jan'60 *61 * will result in all samples from a human subject named 'Jan'. Samples from a mouse subject62 * named 'Jan' or a human subject named 'Kees' won't satisfy the criteria.63 *64 */65 @Override66 protected void executeAnd() {67 def assays = Assay.list().findAll { it.parent?.canRead( this.user ) };68 69 executeAnd( assays );70 }71 72 /**73 * Searches for samples based on the given criteria. Only one of the criteria have to be satisfied and74 * criteria for the different entities are satisfied as follows:75 *76 * Study.title = 'abc'77 * Only assays are returned from studies with title 'abc'78 *79 * Subject.species = 'human'80 * Only assays are returned with samples from subjects with species = 'human'81 *82 * Sample.name = 'sample 1'83 * Only assays are returned with samples with name = 'sample 1'84 *85 * Event.startTime = '0s'86 * Only assays are returned with samples from subjects that have had an event with start time = '0s'87 *88 * SamplingEvent.startTime = '0s'89 * Only assays are returned with samples that have originated from a sampling event with start time = '0s'90 *91 * Assay.module = 'metagenomics'92 * Only assays are returned with module = metagenomics93 *94 * When searching for more than one criterion per entity, these are taken separately. Searching for95 *96 * Subject.species = 'human'97 * Subject.name = 'Jan'98 *99 * will result in all samples from a human subject or a subject named 'Jan'. Samples from a mouse subject100 * named 'Jan' or a human subject named 'Kees' will also satisfy the criteria.101 *102 */103 @Override104 void executeOr() {105 def allAssays = Assay.list().findAll { it.parent?.canRead( this.user ) }.toList();106 executeOr( allAssays );107 32 } 108 33 … … 145 70 } 146 71 72 73 /** 74 * Returns the HQL name for the element or collections to be searched in, for the given entity name 75 * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50. 76 * But when searching for Sample results, the system must search in sample.parentSubject for age > 50 77 * 78 * @param entity Name of the entity of the criterion 79 * @return HQL name for this element or collection of elements 80 */ 81 protected String elementName( String entity ) { 82 switch( entity ) { 83 case "Assay": return "assay" 84 case "Sample": return "assay.samples" 85 case "Study": return "assay.parent" 86 87 case "Subject": return "assay.samples.parentSubject" // Will not be used, since entityClause() is overridden 88 case "SamplingEvent": return "assay.samples.parentEvent" // Will not be used, since entityClause() is overridden 89 case "Event": return "assay.samples.parentEventGroup.events" // Will not be used, since entityClause() is overridden 90 default: return null; 91 } 92 } 93 94 /** 95 * Returns the a where clause for the given entity name 96 * For example: when searching for Subject.age > 50 with Study results, the system must search 97 * 98 * WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...) 99 * 100 * The returned string is fed to sprintf with 3 string parameters: 101 * from (in this case 'study.subjects' 102 * alias (in this case 'subject' 103 * paramName (in this case '...') 104 * 105 * @param entity Name of the entity of the criterion 106 * @return HQL where clause for this element or collection of elements 107 */ 108 protected String entityClause( String entity ) { 109 switch( entity ) { 110 case "Subject": 111 return 'EXISTS( FROM assay.samples sample WHERE sample.parentSubject IN (:%3$s) )' 112 case "SamplingEvent": 113 return 'EXISTS( FROM assay.samples sample WHERE sample.parentEvent IN (:%3$s) )' 114 case "Event": 115 return 'EXISTS( FROM assay.samples sample WHERE EXISTS( FROM sample.parentEventGroup.events event WHERE event IN (:%3$s) ) )' 116 default: 117 return super.entityClause( entity ); 118 } 119 } 120 121 /** 122 * Returns true iff the given entity is accessible by the user currently logged in 123 * 124 * @param entity Study to determine accessibility for. 125 * @return True iff the user is allowed to access this study 126 */ 127 protected boolean isAccessible( def entity ) { 128 return entity?.parent?.canRead( this.user ); 129 } 130 131 147 132 /** 148 133 * Returns the saved field data that could be shown on screen. This means, the data -
trunk/src/groovy/dbnp/query/Criterion.groovy
r1800 r1820 30 30 31 31 /** 32 * Returns the class for the entity of this criterion 33 * @return 34 */ 35 public Class entityClass() { 36 if( this.entity == '*' ) 37 return null; 38 39 40 try { 41 return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity) 42 } catch( Exception e ) { 43 throw new Exception( "Unknown entity for criterion " + this, e ); 44 } 45 } 46 47 /** 32 48 * Retrieves a combination of the entity and field 33 49 * @return … … 36 52 return entity.toString() + ( field ? "." + field.toString() : "" ); 37 53 } 38 54 39 55 /** 40 56 * Retrieves a human readable description of the combination of the entity and field … … 52 68 } 53 69 } 70 71 /** 72 * Returns the type of criterion when searching. Multiple types can be returned, since fields 73 * with the same name might have different types. 74 * 75 * @return List of strings determining the type of this criterion. Possibilities are: 76 * [STRING,BOOLEAN,..]:The criterion references a template field that contains a 'simple' 77 * value (boolean, double, long, string, reltime, date) 78 * [STRINGLIST,...]: The criterion references a template field that contains a 'complex' 79 * value (listitem, ontologyterm, template, module) referencing another 80 * database table 81 * Wildcard: The criterion references all fields 82 */ 83 protected List<String> criterionType() { 84 if( this.entity == '*' || this.field == '*' ) { 85 return [ 86 'String', 87 'Text', 88 'File', 89 'Date', 90 'RelTime', 91 'Double', 92 'Long', 93 'Boolean', 94 'StringList', 95 'ExtendableStringList', 96 'Term', 97 'Template', 98 'Module' 99 ] 100 } 101 102 // Determine domain fields of the entity 103 def domainFields = entityClass().giveDomainFields(); 104 def domainField = domainFields.find { it.name == this.field }; 105 if( domainField ) 106 return [domainField.type?.casedName]; 107 108 // If this field is not a domain field, search for the field in the database 109 def entityClass = entityClass() 110 111 if( !entityClass || !this.field ) 112 return null; 113 114 // Find all fields with this name and entity 115 def fields = TemplateField.findAllByName( this.field ).findAll { it.entity == entityClass }; 116 117 // If the field is not found, return null 118 if( !fields ) 119 return null 120 121 // Return the (unique) String value of the types 122 return fields*.type.unique()*.casedName; 123 } 124 125 /** 126 * Determines whether the field in this criterion is a domain field 127 * 128 * @return True iff the field is a domain field, false otherwise 129 */ 130 protected boolean isDomainCriterion() { 131 def entityClass = entityClass() 132 133 if( !entityClass ) 134 return false; 135 136 // Determine domain fields of the entity 137 def domainFields = entityClass.giveDomainFields(); 138 def domainField = domainFields.find { it.name == this.field }; 139 140 return (domainField ? true : false) 141 } 54 142 143 /** 144 * Determines whether this criterion references a 'complex' field (i.e. a field that 145 * contains a complex type like Term, ListItem etc.) 146 * 147 * @return 148 */ 149 public boolean isComplexCriterion() { 150 if( isDomainCriterion() ) 151 return false; 152 153 def types = criterionType(); 154 155 return types.any { type -> 156 switch( type ) { 157 case 'StringList': 158 case 'ExtendableStringList': 159 case 'Term': 160 case 'Template': 161 case 'Module': 162 return true; 163 } 164 165 return false; 166 } 167 } 168 169 /** 170 * Case the field value to search on to the given type 171 * @param fieldType Name of the template field type 172 * @return Value casted to the right value 173 */ 174 protected def castValue( String fieldType ) { 175 switch( fieldType ) { 176 177 case 'String': 178 case 'Text': 179 case 'StringList': 180 case 'ExtendableStringList': 181 case 'Term': 182 case 'Template': 183 case 'Module': 184 return value?.toString(); 185 case 'File': 186 return null; // Never search in filenames, since they are not very descriptive 187 case 'Date': 188 // The comparison with date values should only be performed iff the value 189 // contains a parsable date 190 // and the operator is equals, gte, gt, lt or lte 191 if( operator == Operator.insearch || operator == Operator.contains ) 192 return null 193 194 try { 195 Date dateCriterion = new SimpleDateFormat( "yyyy-MM-dd" ).parse( value ); 196 return dateCriterion 197 } catch( Exception e ) { 198 return null; 199 } 200 201 case 'RelTime': 202 // The comparison with date values should only be performed iff the value 203 // contains a long number 204 // and the operator is equals, gte, gt, lt or lte 205 if( operator == Operator.insearch || operator == Operator.contains ) 206 return null 207 208 try { 209 RelTime rt 210 211 // Numbers are taken to be seconds, if a non-numeric value is given, try to parse it 212 if( value.toString().isLong() ) { 213 rt = new RelTime( Long.parseLong( value.toString() ) ); 214 } else { 215 rt = new RelTime( value.toString() ); 216 } 217 218 return rt.getValue() 219 } catch( Exception e ) { 220 return null; 221 } 222 case 'Double': 223 // The comparison with date values should only be performed iff the value 224 // contains a double number 225 // and the operator is equals, gte, gt, lt or lte 226 if( operator == Operator.insearch || operator == Operator.contains ) 227 return null 228 229 if( value.isDouble() ) { 230 return Double.parseDouble( value ) 231 } else { 232 return null; 233 } 234 case 'Long': 235 // The comparison with date values should only be performed iff the value 236 // contains a long 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.isLong() ) { 242 return Long.parseLong( value ) 243 } else { 244 return null; 245 } 246 case 'Boolean': 247 // The comparison with boolean values should only be performed iff the value 248 // contains 'true' or 'false' (case insensitive) 249 // and the operator is equals 250 if( operator != Operator.equals ) 251 return null 252 253 def lowerCaseValue = value.toString().toLowerCase(); 254 if( lowerCaseValue == 'true' || lowerCaseValue == 'false' ) { 255 return Boolean.parseBoolean( this.value ) 256 } else { 257 return null 258 } 259 } 260 } 261 262 /** 263 * Create a HQL where clause from this criterion, in order to be used within a larger HQL statement 264 * 265 * @param objectToSearchIn HQL name of the object to search in 266 * @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 267 */ 268 public Map toHQL( String prefix, String objectToSearchIn = "object" ) { 269 List whereClause = [] 270 String joinClause = ""; 271 Map parameters = [:]; 272 def emptyCriterion = [ "join": null, "where": null, "parameters": null ]; 273 274 // If this criterion is used to search within another search result, we use a special piece of HQL 275 if( this.operator == Operator.insearch ) { 276 if( this.value?.results ) { 277 parameters[ prefix + "SearchResults" ] = this.value?.results 278 279 return [ "join": "", "where": "( " + objectToSearchIn + " in (:" + prefix + "SearchResults) )" , "parameters": parameters ]; 280 } else { 281 return emptyCriterion; 282 } 283 } 284 285 // If no value is given, don't do anything 286 if( !value ) 287 return emptyCriterion; 288 289 // Check whether the field is a domain field 290 if( isDomainCriterion() ) { 291 // Determine the types of this criterion, but there will be only 1 for a domain field 292 def criterionType = criterionType()[0]; 293 294 // Some domain fields don't contain a value, but a reference to another table 295 // These should be handled differently 296 def fieldName = this.field 297 298 if( 299 ( objectToSearchIn == "subject" && fieldName == "species" ) || 300 ( objectToSearchIn == "sample" && fieldName == "material" ) || 301 ( objectToSearchIn == "assay" && fieldName == "module" ) || 302 ( objectToSearchIn == "samplingEvent" && fieldName == "sampleTemplate" ) ) { 303 fieldName += ".name" 304 } 305 306 def query = extendWhereClause( "( %s )", objectToSearchIn + "." + fieldName, prefix, criterionType, castValue( criterionType ) ); 307 return [ "join": "", "where": query.where, "parameters": query.parameters ] 308 } 309 310 // Determine the type of this criterion 311 def criterionTypes = criterionType(); 312 313 if( !criterionTypes ) 314 return emptyCriterion; 315 316 317 // Several types of criteria are handled differently. 318 // The 'wildcard' is handled by searching for all types. 319 // The 'simple' types (string, double) are handled by searching in the associated table 320 // The 'complex' types (stringlist, template etc., referencing another 321 // database table) can't be handled correctly, since the HQL INDEX() function doesn't work on those relations. 322 // We do a search for these types to see whether any field with that type fits this criterion, in order to 323 // filter out false positives later on. 324 criterionTypes.findAll { it }.each { criterionType -> 325 // Cast criterion value to the right type 326 def currentValue = castValue( criterionType ); 327 328 // Determine field name 329 def fieldName = "template" + criterionType + 'Fields' 330 331 switch( criterionType ) { 332 case "Wildcard": 333 // Wildcard search is handled by 334 break; 335 336 case 'String': 337 case 'Text': 338 case 'File': 339 case 'Date': 340 case 'RelTime': 341 case 'Double': 342 case 'Long': 343 case 'Boolean': 344 // 'Simple' field types 345 if( currentValue != null ) { 346 joinClause += " left join " + objectToSearchIn + "." + fieldName + " as " + prefix + "_" + fieldName + " "; 347 348 def condition = this.oneToManyWhereCondition( prefix + "_" + fieldName, prefix, criterionType, currentValue ) 349 whereClause += condition[ "where" ]; 350 351 condition[ "parameters" ].each { 352 parameters[ it.key ] = it.value; 353 } 354 } 355 break; 356 357 case 'StringList': 358 case 'ExtendableStringList': 359 case 'Term': 360 case 'Template': 361 case 'Module': 362 // 'Complex' field types 363 def condition = this.manyToManyWhereCondition( objectToSearchIn, fieldName, prefix, "name", currentValue ) 364 whereClause += condition[ "where" ]; 365 366 condition[ "parameters" ].each { 367 parameters[ it.key ] = it.value; 368 } 369 default: 370 break; 371 } 372 } 373 374 def where = whereClause?.findAll { it } ? "( " + whereClause.join( " OR " ) + " )" : "" 375 376 return [ "join": joinClause, "where": where , "parameters": parameters ]; 377 } 378 379 /** 380 * Extends a given condition with a where clause of this criterion. If you supply "select * from Study where %s", %s will 381 * be replaced by the where clause for the given field. Also, the parameters map will be extended (if needed) 382 * 383 * @param hql Initial HQL string where the clause will be put into 384 * @param fieldName Name of the field that should be referenced 385 * @param uniquePrefix Unique prefix for this criterion 386 * @param fieldType Type of field value to search for 387 * @param fieldValue Field value to search for 388 * @return Map with 'where' key referencing the extended where clause and 'parameters' key referencing a map with parameters. 389 */ 390 protected Map extendWhereClause( String hql, String fieldName, String uniquePrefix, String fieldType, def fieldValue ) { 391 def parameters = [:] 392 393 switch( this.operator ) { 394 case Operator.contains: 395 hql = sprintf( hql, fieldName + " like :" + uniquePrefix + "ValueLike" ); 396 parameters[ uniquePrefix + "ValueLike" ] = "%" + fieldValue + "%" 397 break; 398 case Operator.equals: 399 case Operator.gte: 400 case Operator.gt: 401 case Operator.lte: 402 case Operator.lt: 403 hql = sprintf( hql, fieldName + " " + this.operator.name + " :" + uniquePrefix + "Value" + fieldType ); 404 parameters[ uniquePrefix + "Value" + fieldType ] = fieldValue 405 break; 406 } 407 408 return [ "where": hql, "parameters": parameters] 409 } 410 411 /** 412 * Creates a condition for this criterion, for a given fieldName and value. The fieldName should reference a collection that has a one-to-many 413 * relation with the object being sought 414 * 415 * @param fieldName Name to search in 416 * @param uniquePrefix Unique prefix for this criterion 417 * @param currentValue Map with 'value' referencing the value being sought and 'type' referencing 418 * the type of the value as string. The value should be be casted to the right class for this field. 419 * @return Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters. 420 */ 421 protected Map oneToManyWhereCondition( String fieldName, String uniquePrefix, String fieldType, def fieldValue ) { 422 // Create the where condition for checking the value 423 // First check the name of the field, if needed 424 def condition 425 def parameters = [:] 426 427 if( this.field != '*' ) { 428 condition = "( %s AND index(" + fieldName + ") = :" + uniquePrefix + "Field )" 429 parameters[ uniquePrefix + "Field" ] = this.field 430 } else { 431 condition = "%s"; 432 } 433 434 def whereClause = extendWhereClause( condition, fieldName, uniquePrefix, fieldType, fieldValue ); 435 parameters.each { 436 whereClause.parameters[ it.key ] = it.value; 437 } 438 439 return whereClause; 440 } 441 442 /** 443 * Creates a condition for this criterion, for a given fieldName and value. The fieldName should 444 * reference a collection that has a many-to-many relation with the object being sought (e.g. templateTermFields). 445 * 446 * Unfortunately, there is no way to determine the name of the field in HQL for this many-to-many collections, since the 447 * INDEX() function in HQL doesn't work for many-to-many collections. 448 * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4879 449 * @see http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 450 * 451 * @param fieldName Name to search in 452 * @param uniquePrefix Unique prefix for this criterion 453 * @param currentValue Map with 'value' referencing the value being sought and 'type' referencing 454 * the type of the value as string. The value should be be casted to the right class for this field. 455 * @return Map with 'where' key referencing the where clause and 'parameters' key referencing a map with parameters. 456 */ 457 protected Map manyToManyWhereCondition( String objectToSearchIn, String collection, String uniquePrefix, String searchField, def value ) { 458 // exists( FROM [objectToSearchIn].[collection] as [uniquePrefix][collection] WHERE [searchField] LIKE [value] ) 459 // Create the where condition for checking the value 460 def condition = "exists ( FROM " + objectToSearchIn + "." + collection + " as " + uniquePrefix + "_" + collection + " WHERE %s )"; 461 462 return extendWhereClause( condition, uniquePrefix + "_" + collection + "." + searchField, uniquePrefix, "STRING", value ); 463 } 464 55 465 /** 56 466 * Retrieves the correct value for this criterion in the given object (with template) … … 71 481 fieldValue = entity.template?.name 72 482 } else if( field == "*" ) { 73 fieldValue = entity.giveFields().collect{ 483 fieldValue = entity.giveFields().collect{ 74 484 if( it && it.name ) { 75 Search.prepare( entity.getFieldValue( it.name ), entity.giveFieldType( it.name ) ) 485 Search.prepare( entity.getFieldValue( it.name ), entity.giveFieldType( it.name ) ) 76 486 } 77 487 } … … 79 489 fieldValue = Search.prepare( entity.getFieldValue( field ), entity.giveFieldType( field ) ) 80 490 } 81 491 82 492 return fieldValue 83 493 } catch( Exception e ) { … … 89 499 90 500 /** 91 * Checks if the given object (with template) that satisfies the given criterion.92 *93 * @param entity Entity to check for criterion satisfaction. Should be a child of template entity94 * @param criterion Criterion to match on95 * @return True iff there the entity satisfies the given criterion.96 */97 public boolean matchOneEntity( TemplateEntity entity ) {98 def fieldValue = this.getFieldValue( entity );99 100 // Null is returned, the given field doesn't exist. In that case, this criterion will fail.101 // TODO: Maybe give the user a choice whether he want's to include these studies or not102 if( fieldValue == null )103 return false;104 105 return this.match( fieldValue );106 }107 108 /**109 * Checks for all entities in the given entityList, if there is any object that satisfies the given criterion.110 *111 * @param entityList List with entities. The entities should be child classes of TemplateEntity112 * @param criterion Criterion to match on113 * @return True iff there is any entity in the list that satisfies the given criterion.114 */115 public boolean matchAnyEntity( List<TemplateEntity> entityList ) {116 for( entity in entityList ) {117 if( matchOneEntity( entity ) )118 return true;119 }120 return false;121 }122 123 /**124 * Checks for all entities in the given entityList, if all objects satisfy the given criterion.125 *126 * @param entityList List with entities. The entities should be child classes of TemplateEntity127 * @param criterion Criterion to match on128 * @return True iff all entities satisfy the given criterion.129 */130 public boolean matchAllEntities( List<TemplateEntity> entityList ) {131 for( entity in entityList ) {132 if( !matchOneEntity( entity ) )133 return false;134 }135 return true;136 }137 138 /**139 * Checks for all values in the given List, if there is any value that satisfies the given criterion.140 *141 * @param entityList List with values.142 * @param criterion Criterion to match on143 * @return True iff there is any value in the list that satisfies the given criterion.144 */145 public boolean matchAny( List valueList ) {146 for( value in valueList ) {147 if( match( value ) )148 return true;149 }150 return false;151 }152 153 /**154 * Checks for all values in the given List, if all values satisfy the given criterion.155 *156 * @param entityList List with values.157 * @param criterion Criterion to match on158 * @return True iff all values satisfy the given criterion.159 */160 public boolean matchAll( List entityList ) {161 for( value in valueList ) {162 if( !match( value ) )163 return false;164 }165 return true;166 }167 168 /**169 501 * Tries to match a value against a criterion and returns true if it matches 170 502 * … … 175 507 if( fieldValue == null ) 176 508 return false; 177 509 178 510 // in-search criteria have to be handled separately 179 511 if( this.operator == Operator.insearch ) { 180 512 return this.value?.getResults()?.contains( fieldValue ); 181 } 182 513 } 514 183 515 // Other criteria are handled based on the class of the value given. 184 516 def classname = fieldValue.class.getName(); … … 202 534 default: matches = compareValues( fieldValue.toString().trim().toLowerCase(), this.operator, value.toString().toLowerCase().trim() ); break; 203 535 } 204 536 205 537 return matches; 206 538 } catch( Exception e ) { … … 229 561 return fieldValue <= criterionValue; 230 562 case Operator.contains: 231 563 // Contains operator can only be used on string values 232 564 return fieldValue.toString().contains( criterionValue.toString() ); 233 565 case Operator.equals: … … 314 646 if( lowerCaseValue != 'true' && lowerCaseValue != 'false' ) 315 647 return false; 316 648 317 649 Boolean booleanCriterion = Boolean.parseBoolean( value ); 318 650 return compareValues( fieldValue, this.operator, booleanCriterion ); … … 346 678 } 347 679 } 348 680 349 681 public static Operator parseOperator( String name ) throws Exception { 350 682 switch( name.trim() ) { 351 case "=": 352 case "equals": return Operator.equals;353 case "contains": return Operator.contains; 354 case ">=": 355 case "gte": return Operator.gte;356 case ">": 357 case "gt": return Operator.gt;358 case "<=": 359 case "lte": return Operator.lte;360 case "<": 361 case "lt": return Operator.lt;683 case "=": 684 case "equals": return Operator.equals; 685 case "contains": return Operator.contains; 686 case ">=": 687 case "gte": return Operator.gte; 688 case ">": 689 case "gt": return Operator.gt; 690 case "<=": 691 case "lte": return Operator.lte; 692 case "<": 693 case "lt": return Operator.lt; 362 694 case "in": return Operator.insearch; 363 695 default: 364 throw new Exception( "Operator not found" ); 696 throw new Exception( "Operator not found" ); 365 697 } 366 698 } … … 369 701 return "[Criterion " + entityField() + " " + operator + " " + value + "]"; 370 702 } 371 703 372 704 public boolean equals( Object o ) { 373 705 if( o == null ) 374 706 return false; 375 376 if( !( o instanceof Criterion ) ) 377 return false; 378 707 708 if( !( o instanceof Criterion ) ) 709 return false; 710 379 711 Criterion otherCriterion = (Criterion) o; 380 712 return this.entity == otherCriterion.entity && 381 this.field == otherCriterion.field &&382 383 713 this.field == otherCriterion.field && 714 this.operator == otherCriterion.operator && 715 this.value == otherCriterion.value; 384 716 } 385 717 } -
trunk/src/groovy/dbnp/query/SampleSearch.groovy
r1800 r1820 30 30 31 31 this.entity = "Sample"; 32 }33 34 /**35 * Searches for samples based on the given criteria. All criteria have to be satisfied and36 * criteria for the different entities are satisfied as follows:37 *38 * Sample.title = 'abc'39 * Only samples are returned from studies with title 'abc'40 *41 * Subject.species = 'human'42 * Only samples are returned from subjects with species = 'human'43 *44 * Sample.name = 'sample 1'45 * Only samples are returned with name = 'sample 1'46 *47 * Event.startTime = '0s'48 * Only samples are returned from subjects that have had an event with start time = '0s'49 *50 * SamplingEvent.startTime = '0s'51 * Only samples are returned that have originated from a sampling event with start time = '0s'52 *53 * Assay.module = 'metagenomics'54 * Only samples are returned that have been processed in an assay with module = metagenomics55 *56 * When searching for more than one criterion per entity, these are taken combined. Searching for57 *58 * Subject.species = 'human'59 * Subject.name = 'Jan'60 *61 * will result in all samples from a human subject named 'Jan'. Samples from a mouse subject62 * named 'Jan' or a human subject named 'Kees' won't satisfy the criteria.63 *64 */65 @Override66 void executeAnd() {67 // If no criteria are found, return all samples68 if( !criteria || criteria.size() == 0 ) {69 results = Sample.list().findAll { it.parent?.canRead( this.user ) };70 return;71 }72 73 // We expect the study criteria to be the most discriminative, and discard74 // the most samples. (e.g. by searching on study title or study type). For75 // that reason we first look through the list of studies. However, when the76 // user didn't enter any study criteria, this will be an extra step, but doesn't77 // cost much time to process.78 def samples = []79 if( getEntityCriteria( 'Study' ).size() > 0 ) {80 def studies = Study.list()81 if( studies )82 studies = studies.findAll { it.canRead( this.user ) };83 84 studies = filterStudiesOnStudyCriteria( studies );85 86 if( studies.size() == 0 ) {87 results = [];88 return;89 }90 91 def c = Sample.createCriteria()92 samples = c.list {93 'in'( 'parent', studies )94 }95 96 // Save data about the resulting studies in the97 // result fields array. The data that is now in the array98 // is saved based on the study id, not based on the sample id99 clearResultFields();100 saveResultFields( samples, getEntityCriteria( "Study" ), { sample, criterion ->101 return criterion.getFieldValue( sample.parent );102 });103 } else {104 samples = Sample.list()105 if( samples )106 samples = samples.findAll { it.parent?.canRead( this.user ) }107 }108 109 samples = filterOnSubjectCriteria( samples );110 samples = filterOnSampleCriteria( samples );111 samples = filterOnEventCriteria( samples );112 samples = filterOnSamplingEventCriteria( samples );113 samples = filterOnAssayCriteria( samples );114 115 // Filter on criteria for which the entity is unknown116 samples = filterOnAllFieldsCriteria( samples );117 118 // Filter on module criteria119 samples = filterOnModuleCriteria( samples );120 121 // Save matches122 results = samples;123 }124 125 /**126 * Searches for samples based on the given criteria. Only one of the criteria have to be satisfied and127 * criteria for the different entities are satisfied as follows:128 *129 * Sample.title = 'abc'130 * Samples are returned from studies with title 'abc'131 *132 * Subject.species = 'human'133 * Samples are returned from subjects with species = 'human'134 *135 * Sample.name = 'sample 1'136 * Samples are returned with name = 'sample 1'137 *138 * Event.startTime = '0s'139 * Samples are returned from subjects that have had an event with start time = '0s'140 *141 * SamplingEvent.startTime = '0s'142 * Samples are returned that have originated from a sampling event with start time = '0s'143 *144 * Assay.module = 'metagenomics'145 * Samples are returned that have been processed in an assay with module = metagenomics146 *147 * When searching for more than one criterion per entity, these are taken separately. Searching for148 *149 * Subject.species = 'human'150 * Subject.name = 'Jan'151 *152 * will result in all samples from a human subject or a subject named 'Jan'. Samples from a mouse subject153 * named 'Jan' or a human subject named 'Kees' will also satisfy the criteria.154 *155 */156 @Override157 void executeOr() {158 def allSamples = Sample.list().findAll { it.parent?.canRead( this.user ) }.toList();159 executeOr( allSamples );160 32 } 161 33 … … 202 74 203 75 /** 204 * Filters the given list of studies on the study criteria 205 * @param studies Original list of studies 206 * @return List with all samples that match the Study-criteria 76 * Returns the HQL name for the element or collections to be searched in, for the given entity name 77 * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50. 78 * But when searching for Sample results, the system must search in sample.parentSubject for age > 50 79 * 80 * @param entity Name of the entity of the criterion 81 * @return HQL name for this element or collection of elements 207 82 */ 208 protected List filterStudiesOnStudyCriteria( List studies ) { 209 return filterOnTemplateEntityCriteria(studies, "Study", { study, criterion -> return criterion.getFieldValue( study ) }) 83 protected String elementName( String entity ) { 84 switch( entity ) { 85 case "Sample": return "sample" 86 case "Study": return "sample.parent" 87 case "Subject": return "sample.parentSubject" 88 case "SamplingEvent": return "sample.parentEvent" 89 case "Event": return "sample.parentEventGroup.events" 90 case "Assay": return "sample.assays" // Will not be used, since entityClause() is overridden 91 default: return null; 92 } 210 93 } 211 94 212 95 /** 213 * Filters the given list of samples on the assay criteria 214 * @param samples Original list of samples 215 * @return List with all samples that match the assay-criteria 96 * Returns the a where clause for the given entity name 97 * For example: when searching for Subject.age > 50 with Study results, the system must search 98 * 99 * WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...) 100 * 101 * The returned string is fed to sprintf with 3 string parameters: 102 * from (in this case 'study.subjects' 103 * alias (in this case 'subject' 104 * paramName (in this case '...') 105 * 106 * @param entity Name of the entity of the criterion 107 * @return HQL where clause for this element or collection of elements 216 108 */ 217 @Override 218 protected List filterOnAssayCriteria( List samples ) { 219 if( !samples?.size() ) 220 return []; 109 protected String entityClause( String entity ) { 110 switch( entity ) { 111 case "Assay": 112 return 'EXISTS( FROM Assay assay WHERE assay IN (:%3$s) AND EXISTS( FROM assay.samples assaySample WHERE assaySample = sample ) ) ' 113 default: 114 return super.entityClause( entity ); 115 } 116 } 221 117 222 // There is no sample.assays property, so we have to look for assays another way: just find 223 // all assays that match the criteria 224 def criteria = getEntityCriteria( 'Assay' ); 225 226 if( getEntityCriteria( 'Assay' ).size() == 0 ) { 227 if( searchMode == SearchMode.and ) 228 return samples 229 else if( searchMode == SearchMode.or ) 230 return []; 231 } 232 233 def assays = filterEntityList( Assay.list(), criteria, { assay, criterion -> 234 if( !assay ) 235 return false 236 237 return criterion.matchOneEntity( assay ); 238 }); 239 240 println "Matching assays: " + assays 241 242 // If no assays match these criteria, then no samples will match either 243 if( assays.size() == 0 ) 244 return []; 245 246 // Now filter the samples on whether they are attached to the filtered assays 247 def matchingSamples = samples.findAll { sample -> 248 if( !sample.parent ) 249 return false; 250 251 def studyAssays = assays.findAll { it.parent.equals( sample.parent ); } 252 253 println "Assays for " + sample + " (based on study): " + studyAssays 254 255 // See if this sample is present in any of the matching assays. If so, 256 // this sample matches the criteria 257 for( def assay in studyAssays ) { 258 if( assay.samples?.contains( sample ) ) 259 return true; 260 261 println "Assay " + assay + " with samples " + assay.samples + " doesn't contain " + sample; 262 } 263 264 return false; 265 } 266 267 return matchingSamples; 118 /** 119 * Returns true iff the given entity is accessible by the user currently logged in 120 * 121 * @param entity Study to determine accessibility for. 122 * @return True iff the user is allowed to access this study 123 */ 124 protected boolean isAccessible( def entity ) { 125 return entity?.parent?.canRead( this.user ); 268 126 } 269 127 -
trunk/src/groovy/dbnp/query/Search.groovy
r1800 r1820 112 112 this.executionDate = new Date(); 113 113 114 switch( searchMode ) { 115 case SearchMode.and: 116 executeAnd(); 117 break; 118 case SearchMode.or: 119 executeOr(); 120 break; 121 } 114 // Execute the search 115 executeSearch(); 122 116 123 117 // Save the value of this results for later use … … 126 120 127 121 /** 128 * Executes an inclusive (AND) search based on the given criteria. Should be filled in by 129 * subclasses searching for a specific entity 130 */ 131 protected void executeAnd() { 132 133 } 134 135 /** 136 * Executes an exclusive (OR) search based on the given criteria. Should be filled in by 137 * subclasses searching for a specific entity 138 */ 139 protected void executeOr() { 140 141 } 142 143 /** 144 * Default implementation of an inclusive (AND) search. Can be called by subclasses in order 145 * to simplify searches. 146 * 147 * Filters the list of objects on study, subject, sample, event, samplingevent and assaycriteria, 148 * based on the closures defined in valueCallback. Afterwards, the objects are filtered on module 149 * criteria 150 * 151 * @param objects List of objects to search in 152 */ 153 protected void executeAnd( List objects ) { 154 // If no criteria are found, return all studies 155 if( !criteria || criteria.size() == 0 ) { 156 results = objects; 157 return; 158 } 159 160 // Perform filters 161 objects = filterOnStudyCriteria( objects ); 162 objects = filterOnSubjectCriteria( objects ); 163 objects = filterOnSampleCriteria( objects ); 164 objects = filterOnEventCriteria( objects ); 165 objects = filterOnSamplingEventCriteria( objects ); 166 objects = filterOnAssayCriteria( objects ); 167 168 // Filter on criteria for which the entity is unknown 169 objects = filterOnAllFieldsCriteria( objects ); 170 171 // Filter on module criteria 172 objects = filterOnModuleCriteria( objects ); 173 174 // Save matches 175 results = objects; 176 } 177 178 /** 179 * Default implementation of an exclusive (OR) search. Can be called by subclasses in order 180 * to simplify searches. 181 * 182 * Filters the list of objects on study, subject, sample, event, samplingevent and assaycriteria, 183 * based on the closures defined in valueCallback. Afterwards, the objects are filtered on module 184 * criteria 185 * 186 * @param allObjects List of objects to search in 187 */ 188 protected void executeOr( List allObjects ) { 189 // If no criteria are found, return all studies 190 if( !criteria || criteria.size() == 0 ) { 191 results = allObjects; 192 return; 193 } 194 195 // Perform filters on those objects not yet found by other criteria 196 def objects = [] 197 objects = ( objects + filterOnStudyCriteria( allObjects - objects ) ).unique(); 198 objects = ( objects + filterOnSubjectCriteria( allObjects - objects ) ).unique(); 199 objects = ( objects + filterOnSampleCriteria( allObjects - objects ) ).unique(); 200 objects = ( objects + filterOnEventCriteria( allObjects - objects ) ).unique(); 201 objects = ( objects + filterOnSamplingEventCriteria( allObjects - objects ) ).unique(); 202 objects = ( objects + filterOnAssayCriteria( allObjects - objects ) ).unique(); 203 204 // Filter on criteria for which the entity is unknown 205 objects = ( objects + filterOnAllFieldsCriteria( allObjects - objects ) ).unique(); 206 207 // All objects (including the ones already found by another criterion) are sent to 208 // be filtered on module criteria, in order to have the module give data about all 209 // objects (for showing purposes later on) 210 objects = ( objects + filterOnModuleCriteria( allObjects ) ).unique(); 211 212 // Save matches 213 results = objects; 214 } 215 122 * Executes a query 123 */ 124 protected void executeSearch() { 125 // Create HQL query for criteria for the entity being sought 126 def selectClause = "" 127 def fullHQL = createHQLForEntity( this.entity ); 128 129 // Create SQL for other entities, by executing a subquery first, and 130 // afterwards selecting the study based on the entities found 131 def resultsFound 132 133 def entityNames = [ "Study", "Subject", "Sample", "Assay", "Event", "SamplingEvent" ]; 134 for( entityToSearch in entityNames ) { 135 // Add conditions for all criteria for the given entity. However, 136 // the conditions for the 'main' entity (the entity being sought) are already added 137 if( entity != entityToSearch ) { 138 resultsFound = addEntityConditions( 139 entityToSearch, // Name of the entity to search in 140 TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ), // Class of the entity to search in 141 elementName( entityToSearch ), // HQL name of the collection to search in 142 entityToSearch[0].toLowerCase() + entityToSearch[1..-1], // Alias for the entity to search in 143 fullHQL // Current HQL statement 144 ) 145 146 // If no results are found, and we are searching 'inclusive', there will be no 147 // results whatsoever. So we can quit this method now. 148 if( !resultsFound && searchMode == SearchMode.and ) { 149 return 150 } 151 } 152 } 153 154 // Search in all entities 155 resultsFound = addWildcardConditions( fullHQL, entityNames ) 156 if( !resultsFound && searchMode == SearchMode.and ) { 157 return 158 } 159 160 // Combine all parts to generate a full HQL query 161 def hqlQuery = selectClause + " " + fullHQL.from + ( fullHQL.where ? " WHERE " + fullHQL.where.join( " " + searchMode.toString() + " " ) : "" ); 162 163 // Find all objects 164 def entities = entityClass().findAll( hqlQuery, fullHQL.parameters ); 165 166 // Find criteria that match one or more 'complex' fields 167 // These criteria must be checked extra, since they are not correctly handled 168 // by the HQL criteria. See also Criterion.manyToManyWhereCondition and 169 // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 170 entities = filterForComplexCriteria( entities, getEntityCriteria( this.entity ) ); 171 172 // Filter on module criteria. If the search is 'and', only the entities found until now 173 // should be queried in the module. Otherwise, all entities are sent, in order to retrieve 174 // data (to show on screen) for all entities 175 if( hasModuleCriteria() ) { 176 if( searchMode == SearchMode.and ) { 177 entities = filterOnModuleCriteria( entities ); 178 } else { 179 entities = filterOnModuleCriteria( entityClass().list().findAll { this.isAccessible( it ) } ) 180 } 181 } 182 183 // Determine which studies can be read 184 results = entities; 185 186 } 216 187 217 188 /************************************************************************ … … 279 250 } 280 251 } 281 252 253 /** 254 * Returns the HQL name for the element or collections to be searched in, for the given entity name 255 * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50. 256 * But when searching for Sample results, the system must search in sample.parentSubject for age > 50 257 * 258 * This method should be overridden in child classes 259 * 260 * @param entity Name of the entity of the criterion 261 * @return HQL name for this element or collection of elements 262 */ 263 protected String elementName( String entity ) { 264 switch( entity ) { 265 case "Study": 266 case "Subject": 267 case "Sample": 268 case "Event": 269 case "SamplingEvent": 270 case "Assay": 271 return entity[ 0 ].toLowerCase() + entity[ 1 .. -1 ] 272 default: return null; 273 } 274 } 275 276 /** 277 * Returns the a where clause for the given entity name 278 * For example: when searching for Subject.age > 50 with Study results, the system must search 279 * 280 * WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...) 281 * 282 * The returned string is fed to sprintf with 3 string parameters: 283 * from (in this case 'study.subjects' 284 * alias (in this case 'subject' 285 * paramName (in this case '...') 286 * 287 * This method can be overridden in child classes to enable specific behaviour 288 * 289 * @param entity Name of the entity of the criterion 290 * @return HQL where clause for this element or collection of elements 291 */ 292 protected String entityClause( String entity ) { 293 return ' EXISTS( FROM %1$s %2$s WHERE %2$s IN (:%3$s) )' 294 } 295 296 /** 297 * Returns true iff the given entity is accessible by the user currently logged in 298 * 299 * This method should be overridden in child classes, since the check is different for every type of search 300 * 301 * @param entity Entity to determine accessibility for. The entity is of the type 'this.entity' 302 * @return True iff the user is allowed to access this entity 303 */ 304 protected boolean isAccessible( def entity ) { 305 return false 306 } 307 308 /**************************************************** 309 * 310 * Helper methods for generating HQL statements 311 * 312 ****************************************************/ 313 314 /** 315 * Add all conditions for criteria for a specific entity 316 * 317 * @param entityName Name of the entity to search in 318 * @param entityClass Class of the entity to search 319 * @param from Name of the HQL collection to search in (e.g. study.subjects) 320 * @param alias Alias of the HQL collection objects (e.g. 'subject') 321 * @param fullHQL Original HQL map to be extended (fields 'from', 'where' and 'parameters') 322 * @param determineParentId Closure to determine the id of the final entity to search, based on these objects 323 * @param entityCriteria (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found 324 * @return True if one ore more entities are found, false otherwise 325 */ 326 protected boolean addEntityConditions( String entityName, def entityClass, String from, String alias, def fullHQL, def entityCriteria = null ) { 327 if( entityCriteria == null ) 328 entityCriteria = getEntityCriteria( entityName ) 329 330 // Create HQL for these criteria 331 def entityHQL = createHQLForEntity( entityName, entityCriteria ); 332 333 // If any clauses are generated for these criteria, find entities that match these criteria 334 def whereClauses = entityHQL.where?.findAll { it && it?.trim() != "" } 335 if( whereClauses ) { 336 // First find all entities that match these criteria 337 def hqlQuery = entityHQL.from + " WHERE " + whereClauses.join( searchMode == SearchMode.and ? " AND " : " OR " ); 338 def entities = entityClass.findAll( hqlQuery, entityHQL.parameters ) 339 340 // If there are entities matching these criteria, put a where clause in the full HQL query 341 if( entities ) { 342 // Find criteria that match one or more 'complex' fields 343 // These criteria must be checked extra, since they are not correctly handled 344 // by the HQL criteria. See also Criterion.manyToManyWhereCondition and 345 // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 346 entities = filterForComplexCriteria( entities, entityCriteria ); 347 348 def paramName = from.replaceAll( /\W/, '' ); 349 fullHQL.where << sprintf( entityClause( entityName ), from, alias, paramName ); 350 fullHQL.parameters[ paramName ] = entities 351 return true; 352 } else { 353 results = []; 354 return false 355 } 356 } 357 358 return true; 359 } 360 361 /** 362 * Add all conditions for a wildcard search (all fields in a given entity) 363 * @param fullHQL Original HQL map to be extended (fields 'from', 'where' and 'parameters') 364 * @return True if the addition worked 365 */ 366 protected boolean addWildcardConditions( def fullHQL, def entities) { 367 // Append study criteria 368 def entityCriteria = getEntityCriteria( "*" ); 369 370 // If no wildcard criteria are found, return immediately 371 if( !entityCriteria ) 372 return true 373 374 // Wildcards should be checked within each entity 375 def wildcardHQL = createHQLForEntity( this.entity ); 376 377 // Create SQL for other entities, by executing a subquery first, and 378 // afterwards selecting the study based on the entities found 379 entities.each { entityToSearch -> 380 // Add conditions for all criteria for the given entity. However, 381 // the conditions for the 'main' entity (the entity being sought) are already added 382 if( entity != entityToSearch ) { 383 addEntityConditions( 384 entityToSearch, // Name of the entity to search in 385 TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ), // Class of the entity to search in 386 elementName( entityToSearch ), // HQL name of the collection to search in 387 entityToSearch[0].toLowerCase() + entityToSearch[1..-1], // Alias for the entity to search in 388 wildcardHQL, // Current HQL statement 389 entityCriteria // Only create HQL for these criteria 390 ) 391 } 392 } 393 394 // Add these clauses to the full HQL statement 395 def whereClauses = wildcardHQL.where.findAll { it }; 396 397 if( whereClauses ) { 398 fullHQL.from += wildcardHQL.from 399 fullHQL.where << whereClauses.join( " OR " ) 400 401 wildcardHQL[ "parameters" ].each { 402 fullHQL.parameters[ it.key ] = it.value 403 } 404 } 405 406 return true; 407 } 408 409 /** 410 * Create HQL statement for the given criteria and a specific entity 411 * @param entityName Name of the entity 412 * @param entityCriteria (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found 413 * @param includeFrom (optional) If set to true, the 'FROM entity' is prepended to the from clause. Defaults to true 414 * @return 415 */ 416 def createHQLForEntity( String entityName, def entityCriteria = null, includeFrom = true ) { 417 def fromClause = includeFrom ? "FROM " + entityName + " " + entityName.toLowerCase() : "" 418 def whereClause = [] 419 def parameters = [:] 420 def criterionNum = 0; 421 422 // Append study criteria 423 if( entityCriteria == null ) 424 entityCriteria = getEntityCriteria( entityName ); 425 426 entityCriteria.each { 427 def criteriaHQL = it.toHQL( "criterion" +entityName + criterionNum++, entityName.toLowerCase() ); 428 fromClause += " " + criteriaHQL[ "join" ] 429 whereClause << criteriaHQL[ "where" ] 430 criteriaHQL[ "parameters" ].each { 431 parameters[ it.key ] = it.value 432 } 433 } 434 435 // Add a filter such that only readable studies are returned 436 if( entityName == "Study" ) { 437 438 if( this.user == null ) { 439 // Anonymous readers are only given access when published and public 440 whereClause << "( study.publicstudy = true AND study.published = true )" 441 } else if( !this.user.hasAdminRights() ) { 442 // Administrators are allowed to read every study 443 444 // Owners and writers are allowed to read this study 445 // Readers are allowed to read this study when it is published 446 whereClause << "( study.owner = :sessionUser OR :sessionUser member of study.writers OR ( :sessionUser member of study.readers AND study.published = true ) )" 447 parameters[ "sessionUser" ] = this.user 448 } 449 } 450 451 return [ "from": fromClause, "where": whereClause, "parameters": parameters ] 452 } 453 282 454 /***************************************************** 283 455 * … … 405 577 } 406 578 } 407 408 /** 409 * Filters the given list of studies on the study criteria 410 * @param studies Original list of studies 411 * @param entity Name of the entity to check the criteria for 412 * @param valueCallback Callback having a study and criterion as input, returning the value of the field to check on 413 * @return List with all studies that match the Criteria 414 */ 415 protected List filterOnTemplateEntityCriteria( List studies, String entityName, Closure valueCallback ) { 416 def criteria = getEntityCriteria( entityName ); 417 418 def checkCallback = { study, criterion -> 419 def value = valueCallback( study, criterion ); 420 421 if( value == null ) { 422 return false 423 } 424 425 if( value instanceof Collection ) { 426 return criterion.matchAny( value ) 427 } else { 428 return criterion.match( value ); 429 } 430 } 431 432 return filterEntityList( studies, criteria, checkCallback); 433 } 434 435 /** 436 * Filters the given list of studies on the study criteria 437 * @param studies Original list of studies 438 * @return List with all studies that match the Study criteria 439 */ 440 protected List filterOnStudyCriteria( List studies ) { 441 def entity = "Study" 442 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) ) 443 } 444 445 /** 446 * Filters the given list of studies on the subject criteria 447 * @param studies Original list of studies 448 * @return List with all studies that match the Subject-criteria 449 */ 450 protected List filterOnSubjectCriteria( List studies ) { 451 def entity = "Subject" 452 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) ) 453 } 454 455 /** 456 * Filters the given list of studies on the sample criteria 457 * @param studies Original list of studies 458 * @return List with all studies that match the sample-criteria 459 */ 460 protected List filterOnSampleCriteria( List studies ) { 461 def entity = "Sample" 462 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) ) 463 } 464 465 /** 466 * Filters the given list of studies on the event criteria 467 * @param studies Original list of studies 468 * @return List with all studies that match the event-criteria 469 */ 470 protected List filterOnEventCriteria( List studies ) { 471 def entity = "Event" 472 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) ) 473 } 474 475 /** 476 * Filters the given list of studies on the sampling event criteria 477 * @param studies Original list of studies 478 * @return List with all studies that match the event-criteria 479 */ 480 protected List filterOnSamplingEventCriteria( List studies ) { 481 def entity = "SamplingEvent" 482 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) ) 483 } 484 485 /** 486 * Filters the given list of studies on the assay criteria 487 * @param studies Original list of studies 488 * @return List with all studies that match the assay-criteria 489 */ 490 protected List filterOnAssayCriteria( List studies ) { 491 def entity = "Assay" 492 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) ) 493 } 494 495 496 /** 497 * Filters the given list of entities on criteria that mention all fields (e.g. search for studies with 'bacteria' in any field) 498 * @param objects Original list of entities. 499 * @return List of all entities that match the given criteria 500 */ 501 protected List filterOnAllFieldsCriteria( List objects ) { 502 def criteria = getEntityCriteria( "*" ); 503 504 // Find methods to determine a value for a criterion, based on all entities 505 def valueCallbacks = [:]; 506 def entities = [ "Study", "Subject", "Sample", "Event", "SamplingEvent", "Assay" ]; 507 entities.each { 508 valueCallbacks[ it ] = valueCallback( it ); 509 } 510 511 // Create a closure that checks all entities 512 def checkCallback = { object, criterion -> 513 def value = ""; 514 for( def entity in entities ) { 515 value = valueCallbacks[ entity ]( object, criterion ); 516 579 580 /** 581 * Filters an entity list manually on complex criteria found in the criteria list. 582 * This method is needed because hibernate contains a bug in the HQL INDEX() function. 583 * See also Criterion.manyToManyWhereCondition and 584 *http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615 585 * 586 * @param entities List of entities 587 * @param entityCriteria List of criteria that apply to the type of entities given (e.g. Subject criteria for Subjects) 588 * @return Filtered entity list 589 */ 590 protected filterForComplexCriteria( def entities, def entityCriteria ) { 591 def complexCriteria = entityCriteria.findAll { it.isComplexCriterion() } 592 if( complexCriteria ) { 593 def checkCallback = { entity, criterion -> 594 def value = criterion.getFieldValue( entity ) 595 517 596 if( value == null ) { 518 continue;519 } 520 597 return false 598 } 599 521 600 if( value instanceof Collection ) { 522 if( criterion.matchAny( value ) ) 523 return true; 601 return criterion.matchAny( value ) 524 602 } else { 525 if( criterion.match( value ) ) 526 return true; 527 } 528 } 529 530 // If no match is found, return 531 return false; 532 } 533 534 return filterEntityList( objects, criteria, checkCallback); 535 } 536 603 return criterion.match( value ); 604 } 605 } 606 607 entities = filterEntityList( entities, complexCriteria, checkCallback ); 608 } 609 610 return entities; 611 } 612 537 613 /******************************************************************** 538 614 * … … 541 617 ********************************************************************/ 542 618 619 protected boolean hasModuleCriteria() { 620 621 return AssayModule.list().any { module -> 622 // Remove 'module' from module name 623 def moduleName = module.name.replace( 'module', '' ).trim() 624 def moduleCriteria = getEntityCriteria( moduleName ); 625 return moduleCriteria?.size() > 0 626 } 627 } 628 543 629 /** 544 630 * Filters the given list of entities on the module criteria … … 611 697 } 612 698 613 println this.resultFields;614 615 699 return resultingEntities; 616 700 default: … … 878 962 criteria.containsAll( s.criteria ) ); 879 963 } 964 965 /** 966 * Returns the class for the entity being searched 967 * @return 968 */ 969 public Class entityClass() { 970 if( !this.entity ) 971 return null; 972 973 try { 974 return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity) 975 } catch( Exception e ) { 976 throw new Exception( "Unknown entity for criterion " + this, e ); 977 } 978 } 979 880 980 } -
trunk/src/groovy/dbnp/query/StudySearch.groovy
r1800 r1820 33 33 34 34 /** 35 * Searches for studies based on the given criteria. All criteria have to be satisfied and36 * criteria for the different entities are satisfied as follows:37 *38 * Study.title = 'abc'39 * All returned studies will have title 'abc'40 *41 * Subject.species = 'human'42 * All returned studies will have one or more subjects with species = 'human'43 *44 * Sample.name = 'sample 1'45 * All returned studies will have one or more samples with name = 'sample 1'46 *47 * Event.startTime = '0s'48 * All returned studies will have one or more events with start time = '0s'49 *50 * Assay.module = 'metagenomics'51 * All returned studies will have one or more assays with module = 'metagenomics'52 *53 * When searching the system doesn't look at the connections between different entities. This means,54 * the system doesn't look for human subjects having a sample with name 'sample 1'. The sample 1 might55 * as well belong to a mouse subject and still the study satisfies the criteria.56 *57 * When searching for more than one criterion per entity, these are taken combined. Searching for58 *59 * Subject.species = 'human'60 * Subject.name = 'Jan'61 *62 * will result in all studies having a human subject named 'Jan'. Studies with only a mouse subject63 * named 'Jan' or a human subject named 'Kees' won't satisfy the criteria.64 *65 */66 @Override67 protected void executeAnd() {68 def studies = Study.list().findAll { it.canRead( this.user ) };69 70 executeAnd( studies );71 }72 73 /**74 * Searches for studies based on the given criteria. Only one criteria have to be satisfied and75 * criteria for the different entities are satisfied as follows:76 *77 * Study.title = 'abc'78 * The returned study will have title 'abc'79 *80 * Subject.species = 'human'81 * The returned study will have one or more subjects with species = 'human'82 *83 * Sample.name = 'sample 1'84 * The returned study will have one or more samples with name = 'sample 1'85 *86 * Event.startTime = '0s'87 * The returned study will have one or more events with start time = '0s'88 *89 * Assay.module = 'metagenomics'90 * The returned study will have one or more assays with module = 'metagenomics'91 *92 * When searching the system doesn't look at the connections between different entities. This means,93 * the system doesn't look for human subjects having a sample with name 'sample 1'. The sample 1 might94 * as well belong to a mouse subject and still the study satisfies the criteria.95 *96 * When searching for more than one criterion per entity, these are taken separately. Searching for97 *98 * Subject.species = 'human'99 * Subject.name = 'Jan'100 *101 * will result in all studies having a human subject or a subject named 'Jan'. Studies with only a102 * mouse subject named 'Jan' or a human subject named 'Kees' will satisfy the criteria.103 *104 */105 @Override106 protected void executeOr() {107 def allStudies = Study.list().findAll { it.canRead( this.user ) };108 executeOr( allStudies );109 }110 111 /**112 35 * Returns a closure for the given entitytype that determines the value for a criterion 113 36 * on the given object. The closure receives two parameters: the object and a criterion. … … 137 60 return { study, criterion -> return study.assays?.collect { criterion.getFieldValue( it ); } } 138 61 default: 139 return super.valueCallback( entity );62 return null; 140 63 } 141 64 } 142 65 66 /** 67 * Returns the HQL name for the element or collections to be searched in, for the given entity name 68 * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50. 69 * But when searching for Sample results, the system must search in sample.parentSubject for age > 50 70 * 71 * @param entity Name of the entity of the criterion 72 * @return HQL name for this element or collection of elements 73 */ 74 protected String elementName( String entity ) { 75 switch( entity ) { 76 case "Study": return "study" 77 case "Subject": return "study.subjects" 78 case "Sample": return "study.samples" 79 case "Event": return "study.events" 80 case "SamplingEvent": return "study.samplingEvents" 81 case "Assay": return "study.assays" 82 default: return null; 83 } 84 } 85 86 /** 87 * Returns true iff the given entity is accessible by the user currently logged in 88 * 89 * @param entity Study to determine accessibility for. 90 * @return True iff the user is allowed to access this study 91 */ 92 protected boolean isAccessible( def entity ) { 93 return entity?.canRead( this.user ); 94 } 95 143 96 /** 144 97 * Returns the saved field data that could be shown on screen. This means, the data
Note: See TracChangeset
for help on using the changeset viewer.