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.biojava.bio;
023
024import java.io.Serializable;
025import java.util.Collections;
026import java.util.Iterator;
027import java.util.Map;
028import java.util.NoSuchElementException;
029import java.util.Set;
030
031import org.biojava.utils.AbstractChangeable;
032import org.biojava.utils.ChangeEvent;
033import org.biojava.utils.ChangeSupport;
034import org.biojava.utils.ChangeVetoException;
035
036/**
037 * A utility class to ease the problem of implementing an Annotation to that of
038 * providing an apropreate implementation of Map. Where possible implementations
039 *
040 * This class is only intended as a way to implement
041 * Annotation. If you are not trying to do that, then don't read on. If you
042 * are reading the documentation for an Annotation implementation that extends
043 * this, then don't read on. There is nothing to see here.
044 *
045 * If you are still reading this, then you must be trying to
046 * implement Annotation. To do that, extend this class and implement
047 * <code>getProperties()</code> and <code>propertiesAllocated()</code>. 
048 * Where possible implementations should be backed with a 
049 * <code>LinkedHashMap</code> or similar so properties are iterated in the order
050 * they were added.
051 * 
052 * @author Matthew Pocock
053 * @author Greg Cox
054 *
055 * @since 1.0
056 */
057public abstract class AbstractAnnotation
058  extends
059    AbstractChangeable
060  implements
061    Annotation,
062    Serializable
063{
064  /**
065   * Implement this to return the Map delegate. Modifying this return value will
066   * modify the properties associated with this annotation.
067   *
068   * From code in the 1.2 version of AbstractAnnotation
069   * This is required for the implementation of an Annotation that
070   *            extends AbstractAnnotation. Where possible implementations 
071   *            should be backed with a 
072   *            <code>LinkedHashMap</code> or similar so properties are iterated in the order
073   *            they were added.
074   *
075   * @return a Map containing all properties
076   */
077  protected abstract Map getProperties();
078
079  /**
080   * A convenience method to see if we have allocated the properties
081   * Map.
082   * This is required for the implementation of an Annotation that
083   *            extends AbstractAnnotation.
084   * @return true if the properties have been allocated, false otherwise
085   * 
086   */
087  protected abstract boolean propertiesAllocated();
088
089
090  public Object getProperty(Object key) throws NoSuchElementException {
091    if(propertiesAllocated()) {
092      Map prop = getProperties();
093      if(prop.containsKey(key)) {
094        return prop.get(key);
095      }
096    }
097    throw new NoSuchElementException("Property " + key + " unknown");
098  }
099
100  public void setProperty(Object key, Object value)
101  throws ChangeVetoException {
102    if(!hasListeners()) {
103      getProperties().put(key, value);
104    } else {
105      Map properties = getProperties();
106      ChangeEvent ce = new ChangeEvent(
107        this,
108        Annotation.PROPERTY,
109        new Object[] { key, value },
110        new Object[] { key, properties.get(key)}
111      );
112      ChangeSupport cs = getChangeSupport(Annotation.PROPERTY);
113      synchronized(cs) {
114        cs.firePreChangeEvent(ce);
115        properties.put(key, value);
116        cs.firePostChangeEvent(ce);
117      }
118    }
119  }
120
121  public void removeProperty(Object key)
122    throws ChangeVetoException, NoSuchElementException
123  {
124    if (!getProperties().containsKey(key)) {
125        throw new NoSuchElementException("Can't remove key " + key.toString());
126    }
127
128    if(!hasListeners()) {
129      getProperties().remove(key);
130    } else {
131      Map properties = getProperties();
132      ChangeEvent ce = new ChangeEvent(
133        this,
134        Annotation.PROPERTY,
135        new Object[] { key, null },
136        new Object[] { key, properties.get(key)}
137      );
138      ChangeSupport cs = getChangeSupport(Annotation.PROPERTY);
139      synchronized(cs) {
140        cs.firePreChangeEvent(ce);
141        properties.remove(key);
142        cs.firePostChangeEvent(ce);
143      }
144    }
145  }
146
147  public boolean containsProperty(Object key) {
148    if(propertiesAllocated()) {
149      return getProperties().containsKey(key);
150    } else {
151      return false;
152    }
153  }
154
155  public Set keys() {
156    if(propertiesAllocated()) {
157      return getProperties().keySet();
158    } else {
159      return Collections.EMPTY_SET;
160    }
161  }
162
163  public String toString() {
164    StringBuffer sb = new StringBuffer("{");
165    Map prop = getProperties();
166    Iterator i = prop.keySet().iterator();
167    if(i.hasNext()) {
168      Object key = i.next();
169      sb.append(key + "=" + prop.get(key));
170    }
171    while(i.hasNext()) {
172      Object key = i.next();
173      sb.append("," + key + "=" + prop.get(key));
174    }
175    sb.append("}");
176    return sb.substring(0);
177  }
178
179  public Map asMap() {
180    return Collections.unmodifiableMap(getProperties());
181  }
182
183  /**
184   * Protected no-args constructor intended for sub-classes. This class is
185   * abstract and can not be directly instantiated.
186   */
187  protected AbstractAnnotation() {
188  }
189
190  /**
191   * Copy-constructor.
192   *
193   * <p>
194   * This does a shallow copy of the annotation. The result is an annotation
195   * with the same properties and values, but which is independant of the
196   * original annotation.
197   * </p>
198   *
199   * @param ann  the Annotation to copy
200   */
201  protected AbstractAnnotation(Annotation ann) {
202    if(ann == null) {
203      throw new NullPointerException(
204        "Null annotation not allowed. Use Annotation.EMPTY_ANNOTATION instead."
205      );
206    }
207    if(ann == Annotation.EMPTY_ANNOTATION) {
208      return;
209    }
210    Map properties = getProperties();
211    for(Iterator i = ann.keys().iterator(); i.hasNext(); ) {
212      Object key = i.next();
213      try {
214        properties.put(key, ann.getProperty(key));
215      } catch (IllegalArgumentException iae) {
216        throw new BioError(
217          "Property was there and then disappeared: " + key, iae
218        );
219      }
220    }
221  }
222
223  /**
224   * Create a new Annotation by copying the key-value pairs from a map. The
225   * resulting Annotation is independant of the map.
226   *
227   * @param annMap  the Map to copy from.
228   */
229  public AbstractAnnotation(Map annMap) {
230    if(annMap == null) {
231      throw new IllegalArgumentException(
232        "Null annotation Map not allowed. Use an empy map instead."
233      );
234    }
235    if(annMap.isEmpty()) {
236      return;
237    }
238
239    Map properties = getProperties();
240    for(Iterator i = annMap.keySet().iterator(); i.hasNext(); ) {
241      Object key = i.next();
242      properties.put(key, annMap.get(key));
243    }
244  }
245
246
247  public int hashCode() {
248    return asMap().hashCode();
249  }
250
251  public boolean equals(Object o) {
252    if(o == this){
253        return true;
254    }
255    if (! (o instanceof Annotation)) {
256      return false;
257    }
258
259    return ((Annotation) o).asMap().equals(asMap());
260  }
261}