JAL-2664 Updates following review
[jalview.git] / src / jalview / datamodel / PDBEntry.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.datamodel;
22
23 import jalview.util.CaseInsensitiveString;
24
25 import java.util.Collections;
26 import java.util.Enumeration;
27 import java.util.Hashtable;
28
29 public class PDBEntry
30 {
31
32   /**
33    * constant for storing chain code in properties table
34    */
35   private static final String CHAIN_ID = "chain_code";
36
37   private Hashtable<String, Object> properties;
38
39   private static final int PDB_ID_LENGTH = 4;
40
41   private String file;
42
43   private String type;
44
45   private String id;
46
47   public enum Type
48   {
49     // TODO is FILE needed; if not is this enum needed, or can we
50     // use FileFormatI for PDB, MMCIF?
51     PDB("pdb", "pdb"), MMCIF("mmcif", "cif"), FILE("?", "?");
52
53     /*
54      * file extension for cached structure file; must be one that
55      * is recognised by Chimera 'open' command
56      * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
57      */
58     String ext;
59
60     /*
61      * format specifier used in dbfetch request
62      * @see http://www.ebi.ac.uk/Tools/dbfetch/dbfetch/dbfetch.databases#pdb
63      */
64     String format;
65
66     private Type(String fmt, String ex)
67     {
68       format = fmt;
69       ext = ex;
70     }
71
72     public String getFormat()
73     {
74       return format;
75     }
76     public String getExtension()
77     {
78       return ext;
79     }
80
81     /**
82      * case insensitive matching for Type enum
83      * 
84      * @param value
85      * @return
86      */
87     public static Type getType(String value)
88     {
89       for (Type t : Type.values())
90       {
91         if (t.toString().equalsIgnoreCase(value))
92         {
93           return t;
94         }
95       }
96       return null;
97     }
98
99     /**
100      * case insensitive equivalence for strings resolving to PDBEntry type
101      * 
102      * @param t
103      * @return
104      */
105     public boolean matches(String t)
106     {
107       return (this.toString().equalsIgnoreCase(t));
108     }
109   }
110
111   /**
112    * Answers true if obj is a PDBEntry with the same id and chain code (both
113    * ignoring case), file, type and properties
114    */
115   @Override
116   public boolean equals(Object obj)
117   {
118     if (obj == null || !(obj instanceof PDBEntry))
119     {
120       return false;
121     }
122     if (obj == this)
123     {
124       return true;
125     }
126     PDBEntry o = (PDBEntry) obj;
127
128     /*
129      * note that chain code is stored as a property wrapped by a 
130      * CaseInsensitiveString, so we are in effect doing a 
131      * case-insensitive comparison of chain codes
132      */
133     boolean idMatches = id == o.id
134             || (id != null && id.equalsIgnoreCase(o.id));
135     boolean fileMatches = file == o.file
136             || (file != null && file.equals(o.file));
137     boolean typeMatches = type == o.type
138             || (type != null && type.equals(o.type));
139     if (idMatches && fileMatches && typeMatches)
140     {
141       return properties == o.properties
142               || (properties != null && properties.equals(o.properties));
143     }
144     return false;
145   }
146
147   /**
148    * Default constructor
149    */
150   public PDBEntry()
151   {
152   }
153
154
155   public PDBEntry(String pdbId, String chain, PDBEntry.Type type,
156           String filePath)
157   {
158     init(pdbId, chain, type, filePath);
159   }
160
161   /**
162    * @param pdbId
163    * @param chain
164    * @param entryType
165    * @param filePath
166    */
167   void init(String pdbId, String chain, PDBEntry.Type entryType, String filePath)
168   {
169     this.id = pdbId;
170     this.type = entryType == null ? null : entryType.toString();
171     this.file = filePath;
172     setChainCode(chain);
173   }
174
175   /**
176    * Copy constructor.
177    * 
178    * @param entry
179    */
180   public PDBEntry(PDBEntry entry)
181   {
182     file = entry.file;
183     type = entry.type;
184     id = entry.id;
185     if (entry.properties != null)
186     {
187       properties = (Hashtable<String, Object>) entry.properties.clone();
188     }
189   }
190
191   /**
192    * Make a PDBEntry from a DBRefEntry. The accession code is used for the PDB
193    * id, but if it is 5 characters in length, the last character is removed and
194    * set as the chain code instead.
195    * 
196    * @param dbr
197    */
198   public PDBEntry(DBRefEntry dbr)
199   {
200     if (!DBRefSource.PDB.equals(dbr.getSource()))
201     {
202       throw new IllegalArgumentException("Invalid source: "
203               + dbr.getSource());
204     }
205
206     String pdbId = dbr.getAccessionId();
207     String chainCode = null;
208     if (pdbId.length() == PDB_ID_LENGTH + 1)
209     {
210       char chain = pdbId.charAt(PDB_ID_LENGTH);
211       if (('a' <= chain && chain <= 'z') || ('A' <= chain && chain <= 'Z'))
212       {
213         pdbId = pdbId.substring(0, PDB_ID_LENGTH);
214         chainCode = String.valueOf(chain);
215       }
216     }
217     init(pdbId, chainCode, null, null);
218   }
219
220   public void setFile(String f)
221   {
222     this.file = f;
223   }
224
225   public String getFile()
226   {
227     return file;
228   }
229
230   public void setType(String t)
231   {
232     this.type = t;
233   }
234
235   public void setType(PDBEntry.Type type)
236   {
237     this.type = type == null ? null : type.toString();
238   }
239
240   public String getType()
241   {
242     return type;
243   }
244
245   public void setId(String id)
246   {
247     this.id = id;
248   }
249
250   public String getId()
251   {
252     return id;
253   }
254
255   public void setProperty(String key, Object value)
256   {
257     if (this.properties == null)
258     {
259       this.properties = new Hashtable<String, Object>();
260     }
261     properties.put(key, value);
262   }
263
264   public Object getProperty(String key)
265   {
266     return properties == null ? null : properties.get(key);
267   }
268
269   /**
270    * Returns an enumeration of the keys of this object's properties (or an empty
271    * enumeration if it has no properties)
272    * 
273    * @return
274    */
275   public Enumeration<String> getProperties()
276   {
277     if (properties == null)
278     {
279       return Collections.emptyEnumeration();
280     }
281     return properties.keys();
282   }
283
284   /**
285    * 
286    * @return null or a string for associated chain IDs
287    */
288   public String getChainCode()
289   {
290     return (properties == null || properties.get(CHAIN_ID) == null) ? null
291             : properties.get(CHAIN_ID).toString();
292   }
293
294   /**
295    * Sets a non-case-sensitive property for the given chain code. Two PDBEntry
296    * objects which differ only in the case of their chain code are considered
297    * equal. This avoids duplication of objects in lists of PDB ids.
298    * 
299    * @param chainCode
300    */
301   public void setChainCode(String chainCode)
302   {
303     if (chainCode == null)
304     {
305       deleteProperty(CHAIN_ID);
306     }
307     else
308     {
309       setProperty(CHAIN_ID, new CaseInsensitiveString(chainCode));
310     }
311   }
312
313   /**
314    * Deletes the property with the given key, and returns the deleted value (or
315    * null)
316    */
317   Object deleteProperty(String key)
318   {
319     Object result = null;
320     if (properties != null)
321     {
322       result = properties.remove(key);
323     }
324     return result;
325   }
326
327   @Override
328   public String toString()
329   {
330     return id;
331   }
332
333   /**
334    * Getter provided for Castor binding only. Application code should call
335    * getProperty() or getProperties() instead.
336    * 
337    * @deprecated
338    * @see #getProperty(String)
339    * @see #getProperties()
340    * @see jalview.ws.dbsources.Uniprot#getUniprotEntries
341    * @return
342    */
343   @Deprecated
344   public Hashtable<String, Object> getProps()
345   {
346     return properties;
347   }
348
349   /**
350    * Setter provided for Castor binding only. Application code should call
351    * setProperty() instead.
352    * 
353    * @deprecated
354    * @return
355    */
356   @Deprecated
357   public void setProps(Hashtable<String, Object> props)
358   {
359     properties = props;
360   }
361
362   /**
363    * Answers true if this object is either equivalent to, or can be 'improved'
364    * by, the given entry.
365    * <p>
366    * If newEntry has the same id (ignoring case), and doesn't have a conflicting
367    * file spec or chain code, then update this entry from its file and/or chain
368    * code.
369    * 
370    * @param newEntry
371    * @return true if modifications were made
372    */
373   public boolean updateFrom(PDBEntry newEntry)
374   {
375     if (this.equals(newEntry))
376     {
377       return true;
378     }
379
380     String newId = newEntry.getId();
381     if (newId == null || getId() == null)
382     {
383       return false; // shouldn't happen
384     }
385
386     /*
387      * id has to match (ignoring case)
388      */
389     if (!getId().equalsIgnoreCase(newId))
390     {
391       return false;
392     }
393
394     /*
395      * Don't update if associated with different structure files
396      */
397     String newFile = newEntry.getFile();
398     if (newFile != null && getFile() != null && !newFile.equals(getFile()))
399     {
400       return false;
401     }
402
403     /*
404      * Don't update if associated with different chains (ignoring case)
405      */
406     String newChain = newEntry.getChainCode();
407     if (newChain != null && newChain.length() > 0 && getChainCode() != null
408             && getChainCode().length() > 0
409             && !getChainCode().equalsIgnoreCase(newChain))
410     {
411       return false;
412     }
413
414     /*
415      * set file path if not already set
416      */
417     String newType = newEntry.getType();
418     if (getFile() == null && newFile != null)
419     {
420       setFile(newFile);
421       setType(newType);
422     }
423
424     /*
425      * set file type if new entry has it and we don't
426      * (for the case where file was not updated)
427      */
428     if (getType() == null && newType != null)
429     {
430       setType(newType);
431     }
432
433     /*
434      * set chain if not already set (we excluded differing 
435      * chains earlier) (ignoring case change only)
436      */
437     if (newChain != null && newChain.length() > 0
438             && !newChain.equalsIgnoreCase(getChainCode()))
439     {
440       setChainCode(newChain);
441     }
442
443     /*
444      * copy any new or modified properties
445      */
446     Enumeration<String> newProps = newEntry.getProperties();
447     while (newProps.hasMoreElements())
448     {
449       /*
450        * copy properties unless value matches; this defends against changing
451        * the case of chain_code which is wrapped in a CaseInsensitiveString
452        */
453       String key = newProps.nextElement();
454       Object value = newEntry.getProperty(key);
455       if (!value.equals(getProperty(key)))
456       {
457         setProperty(key, value);
458       }
459     }
460     return true;
461   }
462 }