source: trunk/src/groovy/dbnp/query/Criterion.groovy @ 1820

Last change on this file since 1820 was 1820, checked in by robert@…, 9 years ago

Adjusted querying process in order to improve speed with large amounts of data. The querying is now performed directly in HQL, instead of fetching all possible objects from the database and filtering them.

  • Property svn:keywords set to Rev Author Date
File size: 23.3 KB
Line 
1package dbnp.query
2
3import java.text.SimpleDateFormat
4import org.dbnp.gdt.*
5import org.apache.commons.logging.LogFactory;
6
7/**
8 * Available operators for criteria
9 * @author robert
10 *
11 */
12enum Operator {
13        equals( "=" ), contains( "contains" ), gte( ">="), gt( ">" ), lte( "<=" ), lt( "<" ), insearch( "in" )
14        Operator(String name) { this.name = name }
15        private final String name;
16        public String toString() { return name }
17}
18
19/**
20 * Represents a criterion to search on
21 * @author robert
22 *
23 */
24class Criterion {
25        private static final log = LogFactory.getLog(this);
26        public String entity
27        public String field
28        public Operator operator
29        public def value
30
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        /**
48         * Retrieves a combination of the entity and field
49         * @return
50         */
51        public String entityField() {
52                return entity.toString() + ( field ? "." + field.toString() : "" );
53        }
54
55        /**
56         * Retrieves a human readable description of the combination of the entity and field
57         * @return
58         */
59        public String humanReadableEntityField() {
60                if( field == '*' ) {
61                        if( entity == '*' ) {
62                                return "any field in any object"
63                        } else {
64                                return "any field in " + entity.toString();
65                        }
66                } else {
67                        return entityField();
68                }
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        }
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
465        /**
466         * Retrieves the correct value for this criterion in the given object (with template)
467         *
468         * @param entity                Entity to check for value. Should be a child of template entity
469         * @param criterion             Criterion to match on
470         * @return                              Value of the given field or null if the field doesn't exist
471         */
472        public def getFieldValue( TemplateEntity entity ) {
473                if( entity == null )
474                        return null;
475
476                try {
477                        def fieldValue
478                        if( !field ) {
479                                fieldValue = entity
480                        } else if( field == "Template" ) {
481                                fieldValue = entity.template?.name
482                        } else if( field == "*" ) {
483                                fieldValue = entity.giveFields().collect{
484                                        if( it && it.name ) {
485                                                Search.prepare( entity.getFieldValue( it.name ), entity.giveFieldType( it.name ) )
486                                        }
487                                }
488                        } else {
489                                fieldValue = Search.prepare( entity.getFieldValue( field ), entity.giveFieldType( field ) )
490                        }
491
492                        return fieldValue
493                } catch( Exception e ) {
494                        // An exception occurs if the given field doesn't exist. In that case, this criterion will fail.
495                        // TODO: Maybe give the user a choice whether he want's to include these studies or not
496                        return null;
497                }
498        }
499
500        /**
501         * Tries to match a value against a criterion and returns true if it matches
502         *
503         * @param value         Value of the field to match
504         * @return                      True iff the value matches this criterion, false otherwise
505         */
506        public boolean match( def fieldValue ) {
507                if( fieldValue == null )
508                        return false;
509
510                // in-search criteria have to be handled separately
511                if( this.operator == Operator.insearch ) {
512                        return this.value?.getResults()?.contains( fieldValue );
513                }
514
515                // Other criteria are handled based on the class of the value given.
516                def classname = fieldValue.class.getName();
517                classname = classname[classname.lastIndexOf( '.' ) + 1..-1].toLowerCase();
518
519                def matches = false;
520                try {
521                        switch( classname ) {
522                                case "integer":                                 matches = longCompare( new Long( fieldValue.longValue() ) ); break;
523                                case "long":                                    matches = longCompare( fieldValue ); break;
524                                case "float":                                   matches = doubleCompare( new Long( fieldValue.doubleValue() ) ); break;
525                                case "double":                                  matches = doubleCompare( fieldValue ); break;
526                                case "boolean":                                 matches = booleanCompare( fieldValue ); break;
527                                case "date":                                    matches = dateCompare( fieldValue); break;
528                                case "reltime":                                 matches = relTimeCompare( fieldValue ); break;
529                                case "assaymodule":
530                                case "template":
531                                case "term":
532                                case "templatefieldlistitem":
533                                case "string":
534                                default:                                                matches = compareValues( fieldValue.toString().trim().toLowerCase(), this.operator, value.toString().toLowerCase().trim() ); break;
535                        }
536
537                        return matches;
538                } catch( Exception e ) {
539                        log.error e.class.getName() + ": " + e.getMessage();
540                        return false;
541                }
542        }
543
544        /**
545         * Tries to match a value against a criterion and returns true if it matches
546         *
547         * @param fieldValue            Value of the field to match
548         * @param operator                      Operator to apply
549         * @param criterionValue        Value of the criterion
550         * @return                                      True iff the value matches this criterion value, false otherwise
551         */
552        protected boolean compareValues( def fieldValue, Operator operator, def criterionValue ) {
553                switch( operator ) {
554                        case Operator.gte:
555                                return fieldValue >= criterionValue;
556                        case Operator.gt:
557                                return fieldValue > criterionValue;
558                        case Operator.lt:
559                                return fieldValue < criterionValue;
560                        case Operator.lte:
561                                return fieldValue <= criterionValue;
562                        case Operator.contains:
563                        // Contains operator can only be used on string values
564                                return fieldValue.toString().contains( criterionValue.toString() );
565                        case Operator.equals:
566                        default:
567                                return fieldValue.equals( criterionValue );
568                }
569
570        }
571
572        /**
573         * Tries to match a date value against a criterion and returns true if it matches
574         *
575         * @param value         Date value of the field to match
576         * @return                      True iff the value matches this criterion, false otherwise
577         */
578        protected boolean dateCompare( Date fieldValue ) {
579                try {
580                        Date dateCriterion = new SimpleDateFormat( "yyyy-MM-dd" ).parse( value );
581                        Date fieldDate = new Date( fieldValue.getTime() );
582
583                        // Clear time in order to just compare dates
584                        dateCriterion.clearTime();
585                        fieldDate.clearTime();
586
587                        return compareValues( fieldDate, this.operator, dateCriterion )
588                } catch( Exception e ) {
589                        log.error e.class.getName() + ": " + e.getMessage();
590                        return false;
591                }
592        }
593
594        /**
595         * Tries to match a long value against a criterion and returns true if it matches
596         *
597         * @param value         Long value of the field to match
598         * @param criterion     Criterion to match on. Should be a map with entries 'operator' and 'value'
599         * @return                      True iff the value matches this criterion, false otherwise
600         */
601        protected boolean longCompare( Long fieldValue ) {
602                Long longCriterion;
603                try {
604                        longCriterion = Long.parseLong( value );
605                } catch( Exception e ) {
606                        try {
607                                // If converting to long doesn't work, try converting to double and rounding it
608                                Double doubleCriterion = Double.parseDouble(value);
609                                longCriterion = new Long( doubleCriterion.longValue() );
610                        } catch( Exception e2 ) {
611                                log.debug "Can't convert value to long for comparison: " + e2.class.getName() + ": " + e2.getMessage();
612                                return false;
613                        }
614                }
615                return compareValues( fieldValue, this.operator, longCriterion );
616        }
617
618        /**
619         * Tries to match a double value against a criterion and returns true if it matches
620         *
621         * @param value         Double value of the field to match
622         * @return                      True iff the value matches this criterion, false otherwise
623         */
624        protected boolean doubleCompare( Double fieldValue ) {
625                try {
626                        Double doubleCriterion = Double.parseDouble( value );
627                        return compareValues( fieldValue, this.operator, doubleCriterion );
628                } catch( Exception e ) {
629                        log.debug "Can't convert value to double for comparison: " + e.class.getName() + ": " + e.getMessage();
630                        return false;
631                }
632        }
633
634
635        /**
636         * Tries to match a boolean value against a criterion and returns true if it matches
637         *
638         * @param value         Boolean value of the field to match
639         * @return                      True iff the value matches this criterion, false otherwise
640         */
641        protected boolean booleanCompare( Boolean fieldValue ) {
642                try {
643                        // The comparison should only be performed iff the value
644                        // contains 'true' or 'false' (case insensitive)
645                        def lowerCaseValue = value.toString().toLowerCase();
646                        if( lowerCaseValue != 'true' && lowerCaseValue != 'false' )
647                                return false;
648
649                        Boolean booleanCriterion = Boolean.parseBoolean( value );
650                        return compareValues( fieldValue, this.operator, booleanCriterion );
651                } catch( Exception e ) {
652                        log.debug "Can't convert value to boolean for comparison: " + e.class.getName() + ": " + e.getMessage();
653                        return false;
654                }
655        }
656
657        /**
658         * Tries to match a relTime value against a criterion and returns true if it matches
659         *
660         * @param value         relTime value of the field to match
661         * @return                      True iff the value matches this criterion, false otherwise
662         */
663        protected boolean relTimeCompare( RelTime fieldValue ) {
664                try {
665                        RelTime rt
666
667                        // Numbers are taken to be seconds, if a non-numeric value is given, try to parse it
668                        if( value.toString().isLong() ) {
669                                rt = new RelTime( Long.parseLong( value.toString() ) );
670                        } else {
671                                rt = new RelTime( value.toString() );
672                        }
673
674                        return compareValues( fieldValue, this.operator, rt );
675                } catch( Exception e ) {
676                        log.debug "Can't convert value to reltime for comparison: " + e.class.getName() + ": " + e.getMessage();
677                        return false;
678                }
679        }
680
681        public static Operator parseOperator( String name ) throws Exception {
682                switch( name.trim() ) {
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;
694                        case "in":                      return Operator.insearch;
695                        default:
696                                throw new Exception( "Operator not found" );
697                }
698        }
699
700        public String toString() {
701                return "[Criterion " + entityField() + " " + operator + " " + value + "]";
702        }
703
704        public boolean equals( Object o ) {
705                if( o == null )
706                        return false;
707
708                if( !( o instanceof Criterion ) )
709                        return false;
710
711                Criterion otherCriterion = (Criterion) o;
712                return  this.entity == otherCriterion.entity &&
713                this.field == otherCriterion.field &&
714                this.operator == otherCriterion.operator &&
715                this.value == otherCriterion.value;
716        }
717}
Note: See TracBrowser for help on using the repository browser.