/* SimpleDocumentEntry.java
 *
 * created: Tue Feb 15 2000
 *
 * This file is part of Artemis
 *
 * Copyright (C) 2000  Genome Research Limited
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Header: /nfs/disk222/yeastpub/Repository/powmap/uk/ac/sanger/pathogens/embl/SimpleDocumentEntry.java,v 1.7 2000/08/31 13:50:02 kmr Exp $
 */

package uk.ac.sanger.pathogens.embl;

import uk.ac.sanger.pathogens.*;

import java.io.*;
import java.util.Date;

/**
 *  This class contains the methods common to all DocumentEntry objects.
 *
 *  @author Kim Rutherford <kmr@sanger.ac.uk>
 *  @version $Id: SimpleDocumentEntry.java,v 1.7 2000/08/31 13:50:02 kmr Exp $
 **/

abstract public class SimpleDocumentEntry
  implements DocumentEntry {
  /**
   *  Create a new SimpleDocumentEntry from the given Document.
   *  @param entry_information The EntryInformation object of the new Entry.
   *  @param document This is the file that we will read from.  This is also
   *    used for saving the entry back to the file it came from and to give
   *    the new object a name.
   *  @exception IOException thrown if there is a problem reading the entry -
   *    most likely ReadFormatException.
   *  @exception EntryInformationException Thrown if force is false and if this
   *    Entry cannot contain the Key, Qualifier or Key/Qualifier combination of
   *    one of the features in the given Entry.
   **/
  public SimpleDocumentEntry (final EntryInformation entry_information,
                              final Document document)
      throws IOException, EntryInformationException {
    this.document = document;
    this.entry_information = new SimpleEntryInformation (entry_information);

    // this flag is used by setDirtyFlag ()
    this.in_constructor = true;

    final Reader in_file = getDocument ().getReader ();

    LinePushBackReader pushback_reader = new LinePushBackReader (in_file);

    LineGroup new_line_group;

    while ((new_line_group =
            LineGroup.readNextLineGroup (pushback_reader)) != null) {

      // add features that don't have "source" as the key to the FeatureTable
      if (new_line_group instanceof SimpleDocumentFeature) {
        final SimpleDocumentFeature new_feature =
          (SimpleDocumentFeature) new_line_group;
          
        if (new_feature.getKey ().equals ("source")) {
          new_feature.setDocumentEntry (this);

          addLineGroup (new_feature);
        } else {
          try {
            add (new_feature);
          } catch (ReadOnlyException e) {
            throw new Error ("internal error - unexpected exception: " + e);
          }
        }
      } else {
        addLineGroup (new_line_group);
      }
    }

    in_file.close ();

    // we added some features above hence:
    last_change_time = null;

    this.in_constructor = false;
  }

  /**
   *  Create a new SimpleDocumentEntry that has no Document associated with
   *  it.
   *  @param entry_information The EntryInformation object of the new Entry.
   **/
  public SimpleDocumentEntry (final EntryInformation entry_information) {
    this.entry_information = new SimpleEntryInformation (entry_information);
  }

  /**
   *  Create a new SimpleDocumentEntry that will be a copy of the given Entry
   *  and has no Document associated with it.  The new GFFDocumentEntry cannot
   *  be saved to a file with save () unless save (Document) has been called
   *  first.  Some qualifier and location information will be lost if the
   *  argument Entry is a different type to this class.
   *  @param entry_information The EntryInformation object of the Entry that
   *    will contain this Feature.
   *  @param force If true then invalid qualifiers and any features with
   *    invalid keys in the new Entry will be quietly thrown away.  "Invalid"
   *    means that the key/qualifier is not allowed to occur in an Entry of
   *    this type (probably determined by the EntryInformation object of this
   *    Entry).  If false an EntryInformationException will be thrown for
   *    invalid keys or qualifiers.
   **/
  public SimpleDocumentEntry (final EntryInformation entry_information,
                              final Entry new_entry, final boolean force)
      throws EntryInformationException {
    this.entry_information = new SimpleEntryInformation (entry_information);

    if (new_entry.getClass ().equals (this.getClass ())) {
      try {
        setHeaderText (new_entry.getHeaderText ());
      } catch (IOException e) {
        System.err.println (e);
        // if it doesn't work just ignore it
      }
    }

    final FeatureEnumeration feature_enum = new_entry.features ();

    while (feature_enum.hasMoreFeatures ()) {
      final Feature new_feature = feature_enum.nextFeature ();

      try {
        if (force) {
          forcedAdd (makeNativeFeature (new_feature, true));
        } else {
          add (makeNativeFeature (new_feature, true));
        }
      } catch (ReadOnlyException e) {
        throw new Error ("internal error - unexpected exception: " + e);
      }
    }

    final Sequence new_sequence = new_entry.getSequence ();

    if (new_sequence != null) {
      setSequence (makeNativeSequence (new_sequence));
    }
  }

  /**
   *  Return the text of the header of this Entry or null if there is no
   *  header.
   **/
  public String getHeaderText () {
    final StringBuffer buffer = new StringBuffer ();

    for (int i = 0 ; i < line_groups.size () ; ++i) {

      final LineGroup current_line_group = line_groups.elementAt (i);

      if (! (current_line_group instanceof FeatureTable) &&
          ! (current_line_group instanceof Sequence)) {
        buffer.append (current_line_group.toString ());
      }
    }

    if (buffer.length () > 0) {
      return buffer.toString ();
    } else {
      return null;
    }
  }

  /**
   *  Set the header of this Entry to be the given text.
   *  @return true if and only if the header was successfully set.  Not all
   *    Entry objects can change their header, so it is up to the calling
   *    function to check the return value.  If null the current header will be
   *    removed.
   *  @exception IOException thrown if there is a problem reading the header
   *    from the String - most likely ReadFormatException.
   **/
  public boolean setHeaderText (final String new_header)
      throws IOException {

    final LineGroupVector new_line_groups = new LineGroupVector ();

    if (new_header != null) {
      final Reader reader = new StringReader (new_header);

      final LinePushBackReader pushback_reader =
        new LinePushBackReader (reader);

      LineGroup new_line_group;

      try {
        while ((new_line_group =
                LineGroup.readNextLineGroup (pushback_reader)) != null) {
          if (new_line_group instanceof MiscLineGroup ||
              new_line_group instanceof Feature &&
              ((Feature)new_line_group).getKey ().equals ("source")) {
            new_line_groups.addElement (new_line_group);
          } else {
            throw new ReadFormatException ("the header must contain only " +
                                           "header lines");
          }
        }
      } catch (InvalidRelationException e) {
        throw new ReadFormatException ("the header must contain only " +
                                       "header lines");
      }
    }

    // now remove all the EmblMisc and GenbankMisc LineGroup objects from
    // this Entry
    for (int i = line_groups.size () - 1 ; i >= 0  ; --i) {
      final LineGroup current_line_group = line_groups.elementAt (i);

      if (current_line_group instanceof MiscLineGroup) {
        line_groups.removeElementAt (i);
      }
    }

    if (new_header != null) {
      // then add the new LineGroup objects
      for (int i = 0 ; i < new_line_groups.size () ; ++i) {
        line_groups.insertElementAt (new_line_groups.elementAt (i), i);
      }
    }

    setDirtyFlag ();

    return true;
  }

  /**
   *  Write this Entry to the given stream.
   *  @param writer The stream to write to.
   *  @exception IOException thrown if there is a problem writing the entry.
   **/
  public void writeToStream (final Writer writer)
      throws IOException {

    for (int i = 0 ; i < line_groups.size () ; ++i) {
      final LineGroup current_line_group = line_groups.elementAt (i);

      current_line_group.writeToStream (writer);
    }

    if (line_groups.size () == 1) {
      // don't write out the "//" end of entry marker if we have only one
      // LineGroup - this makes life easier for external programs that need to
      // read the feature table or sequence
      return;
    } else {
      if (line_groups.size () == 2) {
        final LineGroup second_line_group = line_groups.elementAt (1);

        if (second_line_group instanceof RawStreamSequence ||
            second_line_group instanceof FastaStreamSequence) {
          // don't write out the "//" end of entry marker if this is raw or
          // FASTA sequence
          return;
        }
      }
    }

    LineGroup.writeEndOfEMBLEntry (writer);
  }

  /**
   *  Return a count of the number of Feature objects in this Entry.
   **/
  public int getFeatureCount () {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return 0;
    } else {
      return feature_table.getFeatureCount ();
    }
  }

  /**
   *  Add the given Feature to this Entry.  If the Feature is already in an
   *  Entry then Entry.remove () should be called on that Entry before calling
   *  Entry.add ().  An Error will be thrown otherwise.
   *  @exception ReadOnlyException If this entry is read only.
   *  @exception EntryInformationException Thrown if this Entry
   *    cannot contain the Key, Qualifier or Key/Qualifier combination of the
   *    given Feature or if a required qualifier is missing.
   *  @return A reference that was passed to add (), if that Feature can be
   *    stored directly in this Entry, otherwise returns a reference to a new
   *    Feature, that is a copy of the argument.  The argument reference
   *    should not be used after the call to add (), unless the return
   *    reference happens to be the same as the argument.
   **/
  public Feature add (final Feature feature)
      throws EntryInformationException, ReadOnlyException {
    if (feature.getEntry () != null) {
      throw new Error ("internal error - a feature must have one owner");
    }

    final EntryInformation entry_information = getEntryInformation ();

    if (!entry_information.isValidKey (feature.getKey ())) {
      throw new InvalidKeyException (feature.getKey () + " is not a valid " +
                                     "key for this entry", feature.getKey ());
    }

    final QualifierVector new_qualifiers = feature.getQualifiers ();

    final Key new_key = feature.getKey ();

    // check the qualifiers
    for (int i = 0 ; i < new_qualifiers.size () ; ++i) {
      final Qualifier this_qualifier = new_qualifiers.elementAt (i);

      final String this_qualifier_name = this_qualifier.getName ();

      if (!entry_information.isValidQualifier (new_key, this_qualifier_name)) {
        final String message = new_key + " can't have " + this_qualifier_name +
          " as a qualifier";

        throw new InvalidRelationException (message, new_key, this_qualifier);
      }
    }

    final DocumentFeature native_feature = makeNativeFeature (feature, false);

    final FeatureTable feature_table = getFeatureTable ();

    feature_table.add (native_feature);

    native_feature.setDocumentEntry (this);

    setDirtyFlag ();

    return native_feature;
  }

  /**
   *  Add the given Feature to this Entry.  If the Feature is already in an
   *  Entry then Entry.remove() should be called on that Entry before calling
   *  Entry.forcedAdd() (An Error will be thrown otherwise).  Invalid
   *  qualifiers will be quietly thrown away.  Features with invalid keys will
   *  not be added (and null will be returned).  "Invalid" means that the
   *  key/qualifier is non allowed to occur in an Entry of this type (probably
   *  determined by the EntryInformation object of this Entry).  Any
   *  qualifiers that are required for this Entry will be quietly added (with
   *  a zero-length string as the value).
   *  @exception ReadOnlyException If this entry is read only.
   *  @return A reference that was passed to add (), if that Feature can be
   *    stored directly in this Entry, otherwise returns a reference to a new
   *    Feature, that is a copy of the argument.  The argument reference
   *    should not be used after the call to add (), unless the return
   *    reference happens to be the same as the argument.  Returns null if and
   *    only if the new Feature has a key that is invalid for this Entry.
   **/
  public Feature forcedAdd (final Feature feature)
      throws ReadOnlyException {

    if (feature.getEntry () != null) {
      throw new Error ("internal error - a feature must have one owner");
    }

    final EntryInformation entry_information = getEntryInformation ();

    if (!entry_information.isValidKey (feature.getKey ())) {
      return null;
    }


    final QualifierVector feature_qualifiers = feature.getQualifiers ();

    final QualifierVector fixed_qualifiers = new QualifierVector ();

    final Key new_key = feature.getKey ();

    // set to true if there is an invalid qualifier
    boolean qualifiers_fixed = false;

    // check the qualifiers
    for (int i = 0 ; i < feature_qualifiers.size () ; ++i) {
      final Qualifier this_qualifier = feature_qualifiers.elementAt (i);

      final String this_qualifier_name = this_qualifier.getName ();

      if (entry_information.isValidQualifier (new_key, this_qualifier_name)) {
        fixed_qualifiers.setQualifier (this_qualifier);
      } else {
        qualifiers_fixed = true;
      }
    }

    final DocumentFeature native_feature = makeNativeFeature (feature, false);

    if (qualifiers_fixed) {
      try {
        native_feature.setQualifiers (fixed_qualifiers);
      } catch (EntryInformationException e) {
        throw new Error ("internal error - unexpected exception: " + e);
      }
    } else {
      // otherwise use the feature as is
    }

    final FeatureTable feature_table = getFeatureTable ();

    feature_table.add (native_feature);

    native_feature.setDocumentEntry (this);

    setDirtyFlag ();

    return native_feature;


//      if (feature instanceof PublicDBStreamFeature) {
//        final PublicDBStreamFeature stream_feature =
//          (PublicDBStreamFeature) feature;
//        getFeatureTable ().add (stream_feature);
//        stream_feature.setDocumentEntry (this);

//        setDirtyFlag ();

//        return feature;
//      } else {
//        final PublicDBStreamFeature feature_copy;

//        if (this instanceof EmblDocumentEntry) {
//          feature_copy = new EmblStreamFeature (feature);
//        } else {
//          feature_copy = new GenbankStreamFeature (feature);
//        }

//        getFeatureTable ().add (feature_copy);
//        feature_copy.setDocumentEntry (this);

//        setDirtyFlag ();

//        return feature_copy;
//      }
  }

  /**
   *  Remove the given Feature from this Entry.
   *  @return true if and only if the Feature was in this Entry.
   **/
  public boolean remove (Feature feature) {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return false;
    } else {
      final DocumentFeature feature_from_table =
        (DocumentFeature) feature_table.remove (feature);

      if (feature_from_table == null) {
        return false;
      } else {
        feature_from_table.setDocumentEntry (null);

        if (feature_table.getFeatureCount () == 0) {
          // get rid of the feature table

          removeLineGroup (feature_table);
        }

        setDirtyFlag ();

        return true;
      }
    }
  }

  /**
   *  Return the ith Feature from this Entry.  This Features are returned in a
   *  consistent order, sorted by the first base of each Feature.
   **/
  public Feature getFeatureAtIndex (int i) {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return null;
    } else {
      return feature_table.getFeatureAtIndex (i);
    }
  }

  /**
   *  Return the index of the given Feature.  This does the reverse of
   *  getFeatureAtIndex ().
   **/
  public int indexOf (final Feature feature) {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return -1;
    } else {
      return feature_table.indexOf (feature);
    }
  }

  /**
   *  Returns true if and only if this Entry contains the given feature.
   **/
  public boolean contains (final Feature feature) {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return false;
    } else {
      return feature_table.contains (feature);
    }
  }

  /**
   *  Create a new Feature object of an appropriate type in this Entry.
   *  @param key The new feature key
   *  @param location The Location object for the new feature
   *  @param qualifiers The qualifiers for the new feature (can be null if
   *    there are no qualifiers).
   **/
  public Feature createFeature (final Key key,
                                final Location location,
                                final QualifierVector qualifiers)
      throws EntryInformationException, ReadOnlyException,
      OutOfRangeException {

    final Feature new_feature;

    if (this instanceof EmblDocumentEntry) {
      new_feature = new EmblStreamFeature (key, location, qualifiers);
    } else {
      new_feature = new GenbankStreamFeature (key, location, qualifiers);
    }

    add (new_feature);

    setDirtyFlag ();

    return new_feature;
  }

  /**
   *  Return a vector containing the references of the Feature objects within
   *  the given range.
   *  @param range Return features that overlap this range - ie the start of
   *    the feature is less than or equal to the end of the range and the end
   *    of the feature is greater than or equal to the start of the range.
   *  @return The non-source key features of this feature table the are within
   *    the given range.  The returned object is a copy - changes will not
   *    effect the FeatureTable object itself.
   **/
  public FeatureVector getFeaturesInRange (Range range) {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return new FeatureVector ();
    } else {
      return feature_table.getFeaturesInRange (range);
    }
  }

  /**
   *  Return a vector containing the references of all the Feature objects in
   *  this Entry.
   *  @return The non-source key features of this Entry.  The returned object
   *    is a copy - changes will not effect the Entry object itself.
   **/
  public FeatureVector getAllFeatures () {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return new FeatureVector ();
    } else {
      return feature_table.getAllFeatures ();
    }
  }

  /**
   *  Returns an enumeration of the Feature objects in this
   *  SimpleDocumentEntry.  The returned Enumeration object will generate
   *  all features in this object in turn. The first item generated is the
   *  item at index 0, then the item at index 1, and so on.
   **/
  public FeatureEnumeration features () {
    final FeatureTable feature_table = findFeatureTable ();

    if (feature_table == null) {
      return new FeatureEnumeration () {
        public boolean hasMoreFeatures () {
          return false;
        }

        public Feature nextFeature () {
          return null;
        }
      };
    } else {
      return feature_table.features ();
    }
  }

  /**
   *  Return the Sequence object from this entry or null if it does not
   *  contain one.
   *  @return a Sequence object for this Entry.  the returned object is
   *    not a copy - changes to it will change the Entry object itself
   **/
  public Sequence getSequence () {
    for (int i = 0 ; i < line_groups.size () ; ++i) {

      final LineGroup current_line_group = line_groups.elementAt (i);

      if (current_line_group instanceof Sequence) {
        return (Sequence) current_line_group;
      }
    }

    return null;
  }

  /**
   *  Add a new LineGroup object to this Entry.
   *  @param new_line_group A new LineGroup to add.
   **/
  private void addLineGroup (final LineGroup new_line_group) {
    if (new_line_group instanceof FeatureHeader) {
      // insert FH lines before the Sequence and Features (if any)
      for (int i = 0 ; i < line_groups.size () ; ++i) {
        final LineGroup this_line_group = line_groups.elementAt (i);

        if (this_line_group instanceof Feature ||
            this_line_group instanceof FeatureTable ||
            this_line_group instanceof Sequence) {
          line_groups.insertElementAt (new_line_group, i);
          return;
        }
      }
    } else {
      if (new_line_group instanceof Feature) {
        // insert "source" features before the Sequence and FeatureTable (if
        // any)
        for (int i = 0 ; i < line_groups.size () ; ++i) {
          final LineGroup this_line_group = line_groups.elementAt (i);
          
          if (this_line_group instanceof FeatureTable ||
              this_line_group instanceof Sequence) {
            line_groups.insertElementAt (new_line_group, i);
            return;
          }
        }
      } else {
        if (! (new_line_group instanceof Sequence) &&
            ! (new_line_group instanceof FeatureTable)) {
          // insert before features and sequence
          for (int i = 0 ; i < line_groups.size () ; ++i) {
            final LineGroup this_line_group = line_groups.elementAt (i);
            
            if (this_line_group instanceof Feature ||
                this_line_group instanceof FeatureTable ||
                this_line_group instanceof FeatureHeader ||
                this_line_group instanceof Sequence) {
              line_groups.insertElementAt (new_line_group, i);
              return;
            }
          }
        } else {
          // fall throw
        }
      }
    }

    // otherwise add at end
    line_groups.addElement (new_line_group);
  }

  /**
   *  Return the name of this Entry or null if it has no name.
   **/
  public String getName () {
    if (getDocument () == null || getDocument ().getName () == null) {
      return null;
    } else {
      return getDocument ().getName ();
    }
  }

  /**
   *  Set the name of this Entry - if possible (the return value will let the
   *  caller know).
   *  @return true if and only if the name was successfully set.  Not all
   *    Entry objects can change there name, so it is up to the calling
   *    function to check the return value.
   **/
  public boolean setName (final String name) {
    if (name == null || name.length () == 0) {
      return false;
    }

    setDocument (new FileDocument (new File (name)));

    return true;
  }

  /**
   *  The EntryInformation object that was passed to the constructor.
   **/
  final private EntryInformation entry_information;

  /**
   *  Return the EntryInformation object for this Entry.
   **/
  public EntryInformation getEntryInformation () {
    return entry_information;
  }

  /**
   *  Create and return a new FeatureTable ().  The new FeatureTable will be
   *  added to line_groups in the appropriate place.
   **/
  private FeatureTable createFeatureTable () {
    final FeatureTable new_feature_table;

    new_feature_table = new StreamFeatureTable ();

    if (line_groups.size () > 0 &&
        line_groups.lastElement () instanceof Sequence) {
      // there is a SEQUENCE LineGroup so put the feature table just before
      // it
      line_groups.insertElementAt (new_feature_table,
                                   line_groups.size () - 1);
    } else {
      // no SEQUENCE so add the feature table at the end
      line_groups.insertElementAt (new_feature_table, line_groups.size ());
    }

    return new_feature_table;
  }

  /**
   *  Return the FeatureTable of this DocumentEntry or null if there isn't one
   *  yet.
   **/
  private FeatureTable findFeatureTable () {
    for (int i = 0 ; i < line_groups.size () ; ++i) {

      final LineGroup current_line_group = line_groups.elementAt (i);

      if (current_line_group instanceof FeatureTable) {
        return (FeatureTable) current_line_group;
      }
    }

    return null;
  }


  /**
   *  Return the FeatureTable object from this entry (which will be created if
   *  this object does not already contain one).  This method has package
   *  scope because other package should add, remove and query Features in the
   *  FeatureTable indirectly (through the Entry class).
   *  @return a FeatureTable object for this Entry.  the returned object is
   *    not a copy - changes to it will change the Entry object itself
   **/
  FeatureTable getFeatureTable () {
    final FeatureTable found_feature_table = findFeatureTable ();

    if (found_feature_table == null) {
      return createFeatureTable ();
    } else {
      return found_feature_table;
    }
  }

  /**
   *  Remove the given LineGroup from this SimpleDocumentEntry.
   **/
  private void removeLineGroup (final LineGroup line_group) {
    for (int i = 0 ; i < line_groups.size () ; ++i) {

      final LineGroup current_line_group = line_groups.elementAt (i);

      if (current_line_group == line_group) {
        line_groups.removeElementAt (i);
        setDirtyFlag ();
        return;
      }
    }
  }

  /**
   *  Return the File reference that was passed to the constructor or null if
   *  none was passed.
   **/
  public Document getDocument () {
    return document;
  }

  /**
   *  Set the document to use when saving this DocumentEntry.
   **/
  public void setDocument (final Document document) {
    this.document = document;
  }

  /**
   *  Write this Entry to the Document it came from.  This method uses the
   *  current Document reference (as given by getDocument ()) and will throw a
   *  NullPointerException if the is no current Document ie. if getDocument ()
   *  returns null.  Use save (Document) to save the current Entry
   *  to a different Document.
   *  @exception IOException thrown if there is an IO problem saving the entry.
   **/
  public void save ()
      throws IOException {

    save (getDocument ());
  }

  /**
   *  Write this Entry to the given Document.
   *  @param document This is the file that we will write to.
   *  @exception IOException thrown if there is an IO problem saving the entry.
   **/
  public void save (final Document document)
      throws IOException {

    final Writer out_file = document.getWriter ();
    writeToStream (out_file);
    out_file.close ();
    
    last_change_time = null;
  }

  /**
   *  Returns true if and only if this entry is read only.
   **/
  public boolean isReadOnly () {
    return false;
  }

  /**
   *  Returns true if and only if there have been some changes to this Entry
   *  since the last save.
   **/
  public boolean hasUnsavedChanges () {
    return last_change_time != null;
  }

  /**
   *  Set last_change_time so that hasUnsavedChanges () will return true.
   *  last_change_time can be read with getLastChangeTime ().
   **/
  public void setDirtyFlag () {
    if (in_constructor) {
      // features are being added, but the entry hasn't really changed
    } else {
      if (autosave_thread == null && getName () != null) {
        // this is the first change so start autosaving
        autosave_thread = new DocumentEntryAutosaveThread (this);
        
        autosave_thread.start ();
      }
      
      
      final java.util.Calendar calendar = java.util.Calendar.getInstance ();

      last_change_time = calendar.getTime ();
    }
  }

  /**
   *  Return the Date when this Entry last changed.
   **/
  public Date getLastChangeTime () {
    return last_change_time;
  }

  /**
   *  Add the given Sequence object to this Entry.
   **/
  public void setSequence (final StreamSequence sequence) {
    final Sequence old_sequence = getSequence ();

    if (old_sequence != null) {
      removeLineGroup ((StreamSequence) old_sequence);
    }

    addLineGroup (sequence);
  }

  /**
   *  If the given feature can be added directly to this Entry, then return
   *  it, otherwise create and return a new feature of the appropriate type
   *  (eg. return an EmblStreamFeature if this Entry is a EmblDocumentEntry).
   *  The feature should not be in an Entry when this method is called.
   *  @param copy if true then always make a new copy of the Feature.
   **/
  protected abstract DocumentFeature makeNativeFeature (final Feature feature,
                                                        final boolean copy);

  /**
   *  If the given Sequence can be added directly to this Entry, then return a
   *  copy of it, otherwise create and return a new feature of the appropriate
   *  type for this Entry.
   **/
  protected abstract StreamSequence
    makeNativeSequence (final Sequence sequence);

  /**
   *  The Document object that was passed to the constructor.  This should be
   *  the document that this SimpleDocumentEntry was read from.
   **/
  private Document document = null;

  /**
   *  This contains all the lines (stored as LineGroup objects) from the entry
   *  stream that was passed to the constructor.
   **/
  protected LineGroupVector line_groups = new LineGroupVector ();

  /**
   *  The DocumentEntryAutosaveThread that is started when the first call is
   *  made to setDirtyFlag ().
   **/
  private Thread autosave_thread = null;

  /**
   *  The Date when this Entry last changed or null if this Entry
   *  hasn't changed since the last save.  Set to null by save ().
   **/
  private java.util.Date last_change_time = null;

  /**
   *  Set to true in the constructor while features are added.  setDirtyFlag ()
   *  will do nothing while this is true.
   **/
  private boolean in_constructor = false;
}
