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