001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022package org.biojavax.bio.seq;
023import java.util.Collection;
024import java.util.Iterator;
025import java.util.Set;
026import java.util.TreeSet;
027
028import org.biojava.bio.Annotation;
029import org.biojava.bio.BioException;
030import org.biojava.bio.seq.Feature;
031import org.biojava.bio.seq.FeatureFilter;
032import org.biojava.bio.seq.FeatureHolder;
033import org.biojava.bio.seq.FilterUtils;
034import org.biojava.bio.seq.Sequence;
035import org.biojava.bio.seq.SimpleFeatureHolder;
036import org.biojava.bio.seq.StrandedFeature;
037import org.biojava.bio.symbol.Location;
038import org.biojava.bio.symbol.SymbolList;
039import org.biojava.ontology.InvalidTermException;
040import org.biojava.ontology.Term;
041import org.biojava.utils.AbstractChangeable;
042import org.biojava.utils.ChangeEvent;
043import org.biojava.utils.ChangeSupport;
044import org.biojava.utils.ChangeVetoException;
045import org.biojavax.RankedCrossRef;
046import org.biojavax.RichAnnotation;
047import org.biojavax.RichObjectFactory;
048import org.biojavax.SimpleRichAnnotation;
049import org.biojavax.ontology.ComparableTerm;
050
051/**
052 * A simple implementation of RichFeature.
053 * @author Richard Holland
054 * @author Mark Schreiber
055 * @author Bubba Puryear
056 * @author George Waldon
057 * @since 1.5
058 */
059public class SimpleRichFeature extends AbstractChangeable implements RichFeature {
060    
061        private static int nextRank = 0;
062        
063    private RichAnnotation notes = new SimpleRichAnnotation();
064    private ComparableTerm typeTerm;
065    private ComparableTerm sourceTerm;
066    private FeatureHolder parent;
067    private RichLocation location = RichLocation.EMPTY_LOCATION;
068    private Set<RankedCrossRef> crossrefs = new TreeSet<RankedCrossRef>();
069    private Set<RichFeatureRelationship> relations = new TreeSet<RichFeatureRelationship>();
070    private String name;
071    private int rank = SimpleRichFeature.nextRank++; // Auto-rank!
072    
073    /**
074     * Creates a new instance of SimpleRichFeature based on a template.
075     * @param parent The parent feature holder.
076     * @param templ The template to construct the feature from.
077     * @throws ChangeVetoException if we don't want to be like the template.
078     * @throws InvalidTermException if any of the template terms are bad.
079     */
080    public SimpleRichFeature(FeatureHolder parent, Feature.Template templ) throws ChangeVetoException, InvalidTermException {
081        if (parent==null) throw new IllegalArgumentException("Parent cannot be null");
082        if (templ==null) throw new IllegalArgumentException("Template cannot be null");
083        if (templ.type==null && templ.typeTerm==null) throw new IllegalArgumentException("Template type cannot be null");
084        if (templ.source==null && templ.sourceTerm==null) throw new IllegalArgumentException("Template source cannot be null");
085        if (templ.location==null) throw new IllegalArgumentException("Template location cannot be null");
086        
087        this.setParent(parent);
088        this.setLocation(templ.location);
089        
090        if (templ.typeTerm!=null) this.setTypeTerm(templ.typeTerm);
091        else this.setType(templ.type);
092        if (templ.sourceTerm!=null) this.setSourceTerm(templ.sourceTerm);
093        else this.setSource(templ.source);
094        
095        if (templ.annotation instanceof RichAnnotation) {
096            this.notes.setNoteSet(((RichAnnotation)templ.annotation).getNoteSet());
097        } else {
098            this.notes = new SimpleRichAnnotation();
099            for (Iterator i = templ.annotation.keys().iterator(); i.hasNext(); ) {
100                Object key = i.next();
101                this.notes.setProperty(key, templ.annotation.getProperty(key));
102            }
103        }
104        
105        if (templ instanceof RichFeature.Template) {
106            this.setRankedCrossRefs(((RichFeature.Template)templ).rankedCrossRefs);
107            this.setFeatureRelationshipSet(((RichFeature.Template)templ).featureRelationshipSet);
108        }
109    }
110    
111    // Hibernate requirement - not for public use.
112    protected SimpleRichFeature() {}
113    
114    /**
115     * {@inheritDoc}
116     */
117    public Feature.Template makeTemplate() {
118        RichFeature.Template templ = new RichFeature.Template();
119        templ.annotation = this.notes;
120        templ.featureRelationshipSet = this.relations;
121        templ.rankedCrossRefs = this.crossrefs;
122        templ.location = this.location;
123        templ.sourceTerm = this.sourceTerm;
124        templ.source = this.sourceTerm.getName();
125        templ.typeTerm = this.typeTerm;
126        templ.type = this.typeTerm.getName();
127        return templ;
128    }
129    
130    /**
131     * {@inheritDoc}
132     */
133    public Annotation getAnnotation() { return getRichAnnotation(); }
134    
135    /**
136     * {@inheritDoc}
137     */
138    public RichAnnotation getRichAnnotation() { return this.notes; }
139    
140    /**
141     * {@inheritDoc} 
142     * <b>Warning</b> this method gives access to the original 
143     * Collection not a copy. This is required by Hibernate. If you
144     * modify the object directly the behaviour may be unpredictable.
145     */
146    public Set getNoteSet() { return this.notes.getNoteSet(); }
147    
148    /**
149     * {@inheritDoc} 
150     * <b>Warning</b> this method gives access to the original 
151     * Collection not a copy. This is required by Hibernate. If you
152     * modify the object directly the behaviour may be unpredictable.
153     */
154    public void setNoteSet(Set notes) throws ChangeVetoException { this.notes.setNoteSet(notes); }
155    
156    // Hibernate use only
157    Set getLocationSet() {
158        // Convert the location into a set of BioSQL-compatible simple locations
159//        System.out.println("SimpleRichFeature.getLocationSet-featureId:"+featureId+", locsSet:"+locsSet+", getLocation:"+getLocation());
160        setTerm(locsSet, null);
161        Collection newlocs = RichLocation.Tools.flatten(this.location);
162        this.locsSet.retainAll(newlocs); // clear out forgotten ones
163        this.locsSet.addAll(newlocs); // add in new ones
164        setTerm(locsSet, ((RichLocation) getLocation()).getTerm());
165//        System.out.println("SimpleRichFeature.getLocationSet-featureId:"+featureId+", locsSet:"+locsSet+", this:"+this+", getLocation:"+getLocation());
166        return this.locsSet; // original for Hibernate purposes
167    }
168    
169    
170    private final static void setTerm(final Collection theCollection, final ComparableTerm theTerm) {
171        final Iterator l = theCollection.iterator();
172        while(l.hasNext()) {
173                final RichLocation location = (RichLocation) l.next();
174                        try {
175                                location.setTerm(theTerm);
176                        } catch (Exception e) {
177                                throw new RuntimeException("SimpleRichFeature.setTerm-unable to set term <"+theTerm+"> in location <"+location+">"+e);
178                        }                       
179        }
180    }
181    
182    // Hibernate use only
183    void setLocationSet(Set locs) throws ChangeVetoException {
184        this.locsSet = locs; // original kept for Hibernate purposes
185        // Construct a nice BioJavaX location from the set of BioSQL-compatible simple ones
186        this.location = RichLocation.Tools.construct(RichLocation.Tools.merge(locs));
187        if(locs.size() > 0)((RichLocation) location).setTerm(((RichLocation)locs.iterator().next()).getTerm());
188//        System.out.println("SimpleRichFeature.SETLocationSet-featureId:"+featureId+", locs:"+locs+", location:"+location);
189    }
190    private Set locsSet = new TreeSet();
191    
192    /**
193     * {@inheritDoc}
194     */
195    public void setName(String name) throws ChangeVetoException {
196        if(!this.hasListeners(RichFeature.NAME)) {
197            this.name = name;
198        } else {
199            ChangeEvent ce = new ChangeEvent(
200                    this,
201                    RichFeature.NAME,
202                    name,
203                    this.name
204                    );
205            ChangeSupport cs = this.getChangeSupport(RichFeature.NAME);
206            synchronized(cs) {
207                cs.firePreChangeEvent(ce);
208                this.name = name;
209                cs.firePostChangeEvent(ce);
210            }
211        }
212    }
213    
214    /**
215     * {@inheritDoc}
216     */
217    public String getName() { return this.name; }
218    
219    /**
220     * {@inheritDoc}
221     */
222    public void setRank(int rank) throws ChangeVetoException {
223        if(!this.hasListeners(RichFeature.RANK)) {
224            this.rank = rank;
225        } else {
226            ChangeEvent ce = new ChangeEvent(
227                    this,
228                    RichFeature.RANK,
229                    new Integer(rank),
230                    new Integer(this.rank)
231                    );
232            ChangeSupport cs = this.getChangeSupport(RichFeature.RANK);
233            synchronized(cs) {
234                cs.firePreChangeEvent(ce);
235                this.rank = rank;
236                cs.firePostChangeEvent(ce);
237            }
238        }
239    }
240    
241    /**
242     * {@inheritDoc}
243     */
244    public int getRank() { return this.rank; }
245    
246    /**
247     * {@inheritDoc}
248     */
249    public Sequence getSequence() {
250        FeatureHolder p = this.parent;
251        while (p instanceof Feature) p = ((Feature)p).getParent();
252        return (Sequence)p;
253    }
254    
255    /**
256     * {@inheritDoc}
257     */
258    public String getSource() { return this.sourceTerm.getName(); }
259    
260    /**
261     * {@inheritDoc}
262     */
263    public void setSource(String source) throws ChangeVetoException {
264        try {
265            this.setSourceTerm(RichObjectFactory.getDefaultOntology().getOrCreateTerm(source));
266        } catch (InvalidTermException e) {
267            throw new ChangeVetoException("Source term was rejected by the default ontology",e);
268        }
269    }
270    
271    /**
272     * {@inheritDoc}
273     */
274    public Term getSourceTerm() { return this.sourceTerm; }
275    
276    /**
277     * {@inheritDoc}
278     */
279    public void setSourceTerm(Term t) throws ChangeVetoException, InvalidTermException {
280        if (t==null) throw new IllegalArgumentException("Term cannot be null");
281        ComparableTerm comparableT;
282        if (t instanceof ComparableTerm) {
283                comparableT = (ComparableTerm) t;
284        } else {
285                comparableT = RichObjectFactory.getDefaultOntology().getOrImportTerm(t);
286        }
287        if(!this.hasListeners(RichFeature.SOURCETERM)) {
288            this.sourceTerm = comparableT;
289        } else {
290            ChangeEvent ce = new ChangeEvent(
291                    this,
292                    RichFeature.SOURCETERM,
293                    comparableT,
294                    this.sourceTerm
295                    );
296            ChangeSupport cs = this.getChangeSupport(RichFeature.SOURCETERM);
297            synchronized(cs) {
298                cs.firePreChangeEvent(ce);
299                this.sourceTerm = comparableT;
300                cs.firePostChangeEvent(ce);
301            }
302        }
303    }
304    
305    /**
306     * {@inheritDoc}
307     */
308    public String getType() { return this.typeTerm.getName(); }
309    
310    /**
311     * {@inheritDoc}
312     */
313    public void setType(String type) throws ChangeVetoException {
314        try {
315            this.setTypeTerm(RichObjectFactory.getDefaultOntology().getOrCreateTerm(type));
316        } catch (InvalidTermException e) {
317            throw new ChangeVetoException("Type term was rejected by the default ontology",e);
318        }
319    }
320    
321    /**
322     * {@inheritDoc}
323     */
324    public Term getTypeTerm() { return this.typeTerm; }
325    
326    /**
327     * {@inheritDoc}
328     */
329    public void setTypeTerm(Term t) throws ChangeVetoException, InvalidTermException {
330        if (t==null) throw new IllegalArgumentException("Term cannot be null");
331        ComparableTerm comparableT;
332        if (t instanceof ComparableTerm) {
333                comparableT = (ComparableTerm) t;
334        } else {
335                comparableT = RichObjectFactory.getDefaultOntology().getOrImportTerm(t);
336        }
337        if(!this.hasListeners(RichFeature.TYPETERM)) {
338            this.typeTerm = comparableT;
339        } else {
340            ChangeEvent ce = new ChangeEvent(
341                    this,
342                    RichFeature.TYPETERM,
343                    comparableT,
344                    this.typeTerm
345                    );
346            ChangeSupport cs = this.getChangeSupport(RichFeature.TYPETERM);
347            synchronized(cs) {
348                cs.firePreChangeEvent(ce);
349                this.typeTerm = comparableT;
350                cs.firePostChangeEvent(ce);
351            }
352        }
353    }
354    
355    /**
356     * {@inheritDoc}
357     */
358    public SymbolList getSymbols() { return this.location.symbols(this.getSequence()); }
359    
360    /**
361     * {@inheritDoc}
362     */
363    public Location getLocation() { return this.location; }
364    
365    /**
366     * {@inheritDoc}
367     */
368    public void setLocation(Location loc) throws ChangeVetoException {
369//        System.out.println("SimpleRichFeature.setLocation-featureId:"+featureId+", loc:"+loc);
370        if (loc==null) throw new IllegalArgumentException("Location cannot be null");
371        RichLocation richLoc = RichLocation.Tools.enrich(loc);
372        if(!this.hasListeners(RichFeature.LOCATION)) {
373            this.location = richLoc;
374        } else {
375            ChangeEvent ce = new ChangeEvent(
376                    this,
377                    RichFeature.LOCATION,
378                    richLoc,
379                    this.location
380                    );
381            ChangeSupport cs = this.getChangeSupport(RichFeature.LOCATION);
382            synchronized(cs) {
383                cs.firePreChangeEvent(ce);
384                this.location = richLoc;
385                cs.firePostChangeEvent(ce);
386            }
387        }
388        this.location.setFeature(this);
389//        System.out.println("SimpleRichFeature.setLocation-location:"+location+(location instanceof RichLocation?", term:"+((RichLocation) location).getTerm():""));
390    }
391    
392    /**
393     * {@inheritDoc}
394     */
395    public FeatureHolder getParent() { return this.parent; }
396    
397    /**
398     * {@inheritDoc}
399     */
400    public void setParent(FeatureHolder parent) throws ChangeVetoException {
401        if (parent==null) throw new IllegalArgumentException("Parent cannot be null");
402        if(!this.hasListeners(RichFeature.PARENT)) {
403            this.parent = parent;
404        } else {
405            ChangeEvent ce = new ChangeEvent(
406                    this,
407                    RichFeature.PARENT,
408                    parent,
409                    this.parent
410                    );
411            ChangeSupport cs = this.getChangeSupport(RichFeature.PARENT);
412            synchronized(cs) {
413                cs.firePreChangeEvent(ce);
414                this.parent = parent;
415                cs.firePostChangeEvent(ce);
416            }
417        }
418    }
419    
420    /**
421     * {@inheritDoc} 
422     * <b>Warning</b> this method gives access to the original 
423     * Collection not a copy. This is required by Hibernate. If you
424     * modify the object directly the behaviour may be unpredictable.
425     */
426    public Set<RankedCrossRef> getRankedCrossRefs() { return this.crossrefs; }
427    
428    /**
429     * {@inheritDoc} 
430     * <b>Warning</b> this method gives access to the original 
431     * Collection not a copy. This is required by Hibernate. If you
432     * modify the object directly the behaviour may be unpredictable.
433     */
434    public void setRankedCrossRefs(Set crossrefs) throws ChangeVetoException {
435        this.crossrefs = crossrefs; // original for Hibernate
436    }
437    
438    /**
439     * {@inheritDoc}
440     */
441    public void addRankedCrossRef(RankedCrossRef crossref) throws ChangeVetoException {
442        if (crossref==null) throw new IllegalArgumentException("Crossref cannot be null");
443        if(!this.hasListeners(RichFeature.CROSSREF)) {
444            this.crossrefs.add(crossref);
445        } else {
446            ChangeEvent ce = new ChangeEvent(
447                    this,
448                    RichFeature.CROSSREF,
449                    crossref,
450                    null
451                    );
452            ChangeSupport cs = this.getChangeSupport(RichFeature.CROSSREF);
453            synchronized(cs) {
454                cs.firePreChangeEvent(ce);
455                this.crossrefs.add(crossref);
456                cs.firePostChangeEvent(ce);
457            }
458        }
459    }
460    
461    /**
462     * {@inheritDoc}
463     */
464    public void removeRankedCrossRef(RankedCrossRef crossref) throws ChangeVetoException {
465        if (crossref==null) throw new IllegalArgumentException("Crossref cannot be null");
466        if(!this.hasListeners(RichFeature.CROSSREF)) {
467            this.crossrefs.remove(crossref);
468        } else {
469            ChangeEvent ce = new ChangeEvent(
470                    this,
471                    RichFeature.CROSSREF,
472                    null,
473                    crossref
474                    );
475            ChangeSupport cs = this.getChangeSupport(RichFeature.CROSSREF);
476            synchronized(cs) {
477                cs.firePreChangeEvent(ce);
478                this.crossrefs.remove(crossref);
479                cs.firePostChangeEvent(ce);
480            }
481        }
482    }
483    
484    /**
485     * {@inheritDoc} 
486     * <b>Warning</b> this method gives access to the original 
487     * Collection not a copy. This is required by Hibernate. If you
488     * modify the object directly the behaviour may be unpredictable.
489     */
490    public Set<RichFeatureRelationship> getFeatureRelationshipSet() { return this.relations; } // must be original for Hibernate
491    
492    /**
493     * {@inheritDoc} 
494     * <b>Warning</b> this method gives access to the original 
495     * Collection not a copy. This is required by Hibernate. If you
496     * modify the object directly the behaviour may be unpredictable.
497     */
498    public void setFeatureRelationshipSet(Set<RichFeatureRelationship> relationships) throws ChangeVetoException {
499        this.relations = relationships;  // must be original for Hibernate
500    }
501    
502    /**
503     * {@inheritDoc}
504     */
505    public void addFeatureRelationship(RichFeatureRelationship relationship) throws ChangeVetoException {
506        if (relationship==null) throw new IllegalArgumentException("Relationship cannot be null");
507        if(!this.hasListeners(RichFeature.RELATION)) {
508            this.relations.add(relationship);
509        } else {
510            ChangeEvent ce = new ChangeEvent(
511                    this,
512                    RichFeature.RELATION,
513                    relationship,
514                    null
515                    );
516            ChangeSupport cs = this.getChangeSupport(RichFeature.RELATION);
517            synchronized(cs) {
518                cs.firePreChangeEvent(ce);
519                this.relations.add(relationship);
520                cs.firePostChangeEvent(ce);
521            }
522        }
523    }
524    
525    /**
526     * {@inheritDoc}
527     */
528    public void removeFeatureRelationship(RichFeatureRelationship relationship) throws ChangeVetoException {
529        if (relationship==null) throw new IllegalArgumentException("Relationship cannot be null");
530        if(!this.hasListeners(RichFeature.RELATION)) {
531            this.relations.remove(relationship);
532        } else {
533            ChangeEvent ce = new ChangeEvent(
534                    this,
535                    RichFeature.RELATION,
536                    null,
537                    relationship
538                    );
539            ChangeSupport cs = this.getChangeSupport(RichFeature.RELATION);
540            synchronized(cs) {
541                cs.firePreChangeEvent(ce);
542                this.relations.remove(relationship);
543                cs.firePostChangeEvent(ce);
544            }
545        }
546    }
547    
548    // Converts relations into a set of child feature objects
549    private Set relationsToFeatureSet() {
550        Set features = new TreeSet();
551        for (Iterator i = this.relations.iterator(); i.hasNext(); ) {
552            RichFeatureRelationship r = (RichFeatureRelationship)i.next();
553            features.add(r.getSubject());
554        }
555        return features;
556    }
557    
558    /**
559     * {@inheritDoc}
560     */
561    public boolean containsFeature(Feature f) { return this.relationsToFeatureSet().contains(f); }
562    
563    /**
564     * {@inheritDoc}
565     */
566    public int countFeatures() { return this.relationsToFeatureSet().size(); }
567    
568    /**
569     * {@inheritDoc}
570     */
571    public Feature createFeature(Feature.Template ft) throws BioException, ChangeVetoException {
572        if (ft==null) throw new IllegalArgumentException("Template cannot be null");
573        RichFeature f;
574        try {
575            f = new SimpleRichFeature(this.parent, ft);
576        } catch (InvalidTermException e) {
577            throw new ChangeVetoException("Term was not accepted",e);
578        }
579        this.addFeatureRelationship(
580                new SimpleRichFeatureRelationship(this, f, SimpleRichFeatureRelationship.getContainsTerm(), 0)
581                );
582        return f;
583    }
584    
585    /**
586     * {@inheritDoc}
587     */
588    public Iterator features() { return this.relationsToFeatureSet().iterator(); }
589    
590    /**
591     * {@inheritDoc}
592     */
593    public FeatureHolder filter(FeatureFilter filter) {
594        boolean recurse = !FilterUtils.areProperSubset(filter, FeatureFilter.top_level);
595        return this.filter(filter, recurse);
596    }
597    
598    /**
599     * {@inheritDoc}
600     */
601    public FeatureHolder filter(FeatureFilter fc, boolean recurse) {
602        SimpleFeatureHolder fh = new SimpleFeatureHolder();
603        for (Iterator i = this.features(); i.hasNext(); ) {
604            Feature f = (RichFeature)i.next();
605            try {
606                if (fc.accept(f)) fh.addFeature(f);
607            } catch (ChangeVetoException e) {
608                throw new RuntimeException("Aaargh! Our feature was rejected!",e);
609            }
610        }
611        return fh;
612    }
613    
614    /**
615     * {@inheritDoc}
616     */
617    public FeatureFilter getSchema() { return FeatureFilter.all; }
618    
619    /**
620     * {@inheritDoc}
621     */
622    public void removeFeature(Feature f) throws ChangeVetoException, BioException {
623        for (Iterator i = this.relations.iterator(); i.hasNext(); ) {
624            RichFeatureRelationship r = (RichFeatureRelationship)i.next();
625            if (r.getSubject().equals(f)) i.remove();
626        }
627    }
628    
629    /**
630     * {@inheritDoc}
631     * NOT IMPLEMENTED.
632     */
633    public void setStrand(StrandedFeature.Strand strand) throws ChangeVetoException {
634        throw new ChangeVetoException("The strand is immutable on RichFeature objects.");
635    }
636    
637    /**
638     * {@inheritDoc}
639     */
640    public StrandedFeature.Strand getStrand() {
641        RichLocation.Strand s = this.location.getStrand();
642        if (s.equals(RichLocation.Strand.NEGATIVE_STRAND)) return StrandedFeature.NEGATIVE;
643        if (s.equals(RichLocation.Strand.POSITIVE_STRAND)) return StrandedFeature.POSITIVE;
644        else return StrandedFeature.UNKNOWN;
645    }
646    
647    /**
648     * {@inheritDoc}
649     */
650    public int hashCode() {
651        int code = 17;
652        // Hibernate comparison - we haven't been populated yet
653        if (this.parent==null) return code;
654        // Normal comparison
655        code = 31*code + this.rank;
656        code = 31*code + this.parent.hashCode();
657        code = 31*code + this.sourceTerm.hashCode();
658        code = 31*code + this.typeTerm.hashCode();
659        return code;
660    }
661    
662    /**
663     * {@inheritDoc}
664     * Features are equal when they have the same rank, parent, type, and source.
665     * Features which are not instance of RichFeature are given a 
666     * rank of zero.
667     */
668    public boolean equals(Object o) {
669        if (! (o instanceof Feature)) return false;
670        // Hibernate comparison - we haven't been populated yet
671        if (this.parent==null) return false;
672        // Normal comparison
673        Feature fo = (Feature) o;
674        int ourRank = this.getRank();
675        int theirRank = fo instanceof RichFeature? ((RichFeature)fo).getRank() : 0;
676        if ( ourRank!=theirRank) return false;
677        if (! this.parent.equals(fo.getParent())) return false;
678        if (! this.typeTerm.equals(fo.getTypeTerm())) return false;
679        if (! this.sourceTerm.equals(fo.getSourceTerm())) return false;
680        return true;
681    }
682    
683    /**
684     * {@inheritDoc}
685     * Features are sorted first by rank, then parent, type, and source.
686     * If both parents are not comparable then this part of the sorting
687     * is skipped. Features which are not instance of RichFeature are 
688     * given a rank of zero.
689     */
690    public int compareTo(Object o) {
691        if (o==this) return 0;
692        // Hibernate comparison - we haven't been populated yet
693        if (this.parent==null) return -1;
694        // Normal comparison
695        Feature them = (Feature)o;
696        int ourRank = this.getRank();
697        int theirRank = them instanceof RichFeature? ((RichFeature)them).getRank():0;
698        if (ourRank!=theirRank) return ourRank-theirRank;
699        if (this.parent instanceof Comparable && 
700                        them.getParent() instanceof Comparable && 
701                        !this.parent.equals(them.getParent())) 
702                return ((Comparable)this.parent).compareTo(them.getParent());
703        if (! this.typeTerm.equals(them.getTypeTerm())) 
704                return this.typeTerm.compareTo(them.getTypeTerm());
705        if (! this.sourceTerm.equals(them.getSourceTerm())) 
706                return this.sourceTerm.compareTo(them.getSourceTerm());
707        if(this.parent.equals(them.getParent()))
708            return 0;  // equality on non-comparable parents
709        else
710            return -1;
711    }
712    
713    /**
714     * {@inheritDoc}
715     * Form: "(#rank) parent:type,source(location)"
716     */
717    public String toString() {
718        return "(#"+this.rank+") "+this.parent+":"+this.getType()+","+this.getSource()+"("+this.location+")";
719    }
720    
721    // Hibernate requirement - not for public use.
722    private Integer id;
723    
724    /**
725     * Gets the Hibernate ID. Should be used with caution.
726     * @return the Hibernate ID, if using Hibernate.
727     */
728    public Integer getId() { return this.id; }
729    
730    /**
731     * Sets the Hibernate ID. Should be used with caution.
732     * @param id the Hibernate ID, if using Hibernate.
733     */
734    public void setId(Integer id) { this.id = id;}
735
736}
737