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