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