Merge branch 'releases/Release_2_11_3_Branch'
[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 java.util.Collections;
24 import java.util.Enumeration;
25 import java.util.Hashtable;
26
27 import jalview.io.StructureFile;
28 import jalview.util.CaseInsensitiveString;
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   /**
43    * property set when id is a 'manufactured' identifier from the structure
44    * data's filename
45    */
46   private static final String FAKED_ID = "faked_pdbid";
47
48   /**
49    * property set when the id is authoritative, and should be used in preference
50    * to any identifiers in the structure data
51    */
52   private static final String AUTHORITATIVE_ID = "authoritative_pdbid";
53
54   private String file;
55
56   private String type;
57
58   private String id;
59
60   private StructureFile sf = null;
61
62   public enum Type
63   {
64     // TODO is FILE needed; if not is this enum needed, or can we
65     // use FileFormatI for PDB, MMCIF?
66     PDB("pdb", "pdb"), MMCIF("mmcif", "cif"), BCIF("bcif", "bcif"),
67     FILE("?", "?");
68
69     /*
70      * file extension for cached structure file; must be one that
71      * is recognised by Chimera 'open' command
72      * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
73      */
74     String ext;
75
76     /*
77      * format specifier used in dbfetch request
78      * @see http://www.ebi.ac.uk/Tools/dbfetch/dbfetch/dbfetch.databases#pdb
79      */
80     String format;
81
82     private Type(String fmt, String ex)
83     {
84       format = fmt;
85       ext = ex;
86     }
87
88     public String getFormat()
89     {
90       return format;
91     }
92
93     public String getExtension()
94     {
95       return ext;
96     }
97
98     /**
99      * case insensitive matching for Type enum
100      * 
101      * @param value
102      * @return
103      */
104     public static Type getType(String value)
105     {
106       for (Type t : Type.values())
107       {
108         if (t.toString().equalsIgnoreCase(value))
109         {
110           return t;
111         }
112       }
113       return null;
114     }
115
116     /**
117      * case insensitive equivalence for strings resolving to PDBEntry type
118      * 
119      * @param t
120      * @return
121      */
122     public boolean matches(String t)
123     {
124       return (this.toString().equalsIgnoreCase(t));
125     }
126   }
127
128   /**
129    * Answers true if obj is a PDBEntry with the same id and chain code (both
130    * ignoring case), file, type and properties
131    */
132   @Override
133   public boolean equals(Object obj)
134   {
135     if (obj == null || !(obj instanceof PDBEntry))
136     {
137       return false;
138     }
139     if (obj == this)
140     {
141       return true;
142     }
143     PDBEntry o = (PDBEntry) obj;
144
145     /*
146      * note that chain code is stored as a property wrapped by a 
147      * CaseInsensitiveString, so we are in effect doing a 
148      * case-insensitive comparison of chain codes
149      */
150     boolean idMatches = id == o.id
151             || (id != null && id.equalsIgnoreCase(o.id));
152     boolean fileMatches = file == o.file
153             || (file != null && file.equals(o.file));
154     boolean typeMatches = type == o.type
155             || (type != null && type.equals(o.type));
156     if (idMatches && fileMatches && typeMatches)
157     {
158       return properties == o.properties
159               || (properties != null && properties.equals(o.properties));
160     }
161     return false;
162   }
163
164   /**
165    * Default constructor
166    */
167   public PDBEntry()
168   {
169   }
170
171   public PDBEntry(String pdbId, String chain, PDBEntry.Type type,
172           String filePath)
173   {
174     init(pdbId, chain, type, filePath);
175   }
176
177   /**
178    * @param pdbId
179    * @param chain
180    * @param entryType
181    * @param filePath
182    */
183   void init(String pdbId, String chain, PDBEntry.Type entryType,
184           String filePath)
185   {
186     this.id = pdbId;
187     this.type = entryType == null ? null : entryType.toString();
188     this.file = filePath;
189     setChainCode(chain);
190   }
191
192   /**
193    * Copy constructor.
194    * 
195    * @param entry
196    */
197   public PDBEntry(PDBEntry entry)
198   {
199     file = entry.file;
200     type = entry.type;
201     id = entry.id;
202     if (entry.properties != null)
203     {
204       properties = (Hashtable<String, Object>) entry.properties.clone();
205     }
206   }
207
208   /**
209    * Make a PDBEntry from a DBRefEntry. The accession code is used for the PDB
210    * id, but if it is 5 characters in length, the last character is removed and
211    * set as the chain code instead.
212    * 
213    * @param dbr
214    */
215   public PDBEntry(DBRefEntry dbr)
216   {
217     if (!DBRefSource.PDB.equals(dbr.getSource()))
218     {
219       throw new IllegalArgumentException(
220               "Invalid source: " + dbr.getSource());
221     }
222
223     String pdbId = dbr.getAccessionId();
224     String chainCode = null;
225     if (pdbId.length() == PDB_ID_LENGTH + 1)
226     {
227       char chain = pdbId.charAt(PDB_ID_LENGTH);
228       if (('a' <= chain && chain <= 'z') || ('A' <= chain && chain <= 'Z'))
229       {
230         pdbId = pdbId.substring(0, PDB_ID_LENGTH);
231         chainCode = String.valueOf(chain);
232       }
233     }
234     init(pdbId, chainCode, null, null);
235   }
236
237   public void setFile(String f)
238   {
239     this.file = f;
240   }
241
242   public String getFile()
243   {
244     return file;
245   }
246
247   public void setType(String t)
248   {
249     this.type = t;
250   }
251
252   public void setType(PDBEntry.Type type)
253   {
254     this.type = type == null ? null : type.toString();
255   }
256
257   public String getType()
258   {
259     return type;
260   }
261
262   public void setId(String id)
263   {
264     this.id = id;
265   }
266
267   public String getId()
268   {
269     return id;
270   }
271
272   public void setProperty(String key, Object value)
273   {
274     if (this.properties == null)
275     {
276       this.properties = new Hashtable<String, Object>();
277     }
278     properties.put(key, value);
279   }
280
281   public Object getProperty(String key)
282   {
283     return properties == null ? null : properties.get(key);
284   }
285
286   /**
287    * Returns an enumeration of the keys of this object's properties (or an empty
288    * enumeration if it has no properties)
289    * 
290    * @return
291    */
292   public Enumeration<String> getProperties()
293   {
294     if (properties == null)
295     {
296       return Collections.emptyEnumeration();
297     }
298     return properties.keys();
299   }
300
301   /**
302    * 
303    * @return null or a string for associated chain IDs
304    */
305   public String getChainCode()
306   {
307     return (properties == null || properties.get(CHAIN_ID) == null) ? null
308             : properties.get(CHAIN_ID).toString();
309   }
310
311   /**
312    * Sets a non-case-sensitive property for the given chain code. Two PDBEntry
313    * objects which differ only in the case of their chain code are considered
314    * equal. This avoids duplication of objects in lists of PDB ids.
315    * 
316    * @param chainCode
317    */
318   public void setChainCode(String chainCode)
319   {
320     if (chainCode == null)
321     {
322       deleteProperty(CHAIN_ID);
323     }
324     else
325     {
326       setProperty(CHAIN_ID, new CaseInsensitiveString(chainCode));
327     }
328   }
329
330   /**
331    * Deletes the property with the given key, and returns the deleted value (or
332    * null)
333    */
334   Object deleteProperty(String key)
335   {
336     Object result = null;
337     if (properties != null)
338     {
339       result = properties.remove(key);
340     }
341     return result;
342   }
343
344   @Override
345   public String toString()
346   {
347     return id;
348   }
349
350   /**
351    * Getter provided for Castor binding only. Application code should call
352    * getProperty() or getProperties() instead.
353    * 
354    * @deprecated
355    * @see #getProperty(String)
356    * @see #getProperties()
357    * @see jalview.ws.dbsources.Uniprot#getUniprotEntries
358    * @return
359    */
360   @Deprecated
361   public Hashtable<String, Object> getProps()
362   {
363     return properties;
364   }
365
366   /**
367    * Setter provided for Castor binding only. Application code should call
368    * setProperty() instead.
369    * 
370    * @deprecated
371    * @return
372    */
373   @Deprecated
374   public void setProps(Hashtable<String, Object> props)
375   {
376     properties = props;
377   }
378
379   /**
380    * Answers true if this object is either equivalent to, or can be 'improved'
381    * by, the given entry.
382    * <p>
383    * If newEntry has the same id (ignoring case), and doesn't have a conflicting
384    * file spec or chain code, then update this entry from its file and/or chain
385    * code.
386    * 
387    * @param newEntry
388    * @return true if modifications were made
389    */
390   public boolean updateFrom(PDBEntry newEntry)
391   {
392     if (this.equals(newEntry))
393     {
394       return true;
395     }
396
397     String newId = newEntry.getId();
398     if (newId == null || getId() == null)
399     {
400       return false; // shouldn't happen
401     }
402
403     boolean idMatches = getId().equalsIgnoreCase(newId);
404
405     /*
406      * Don't update if associated with different structure files
407      */
408     String newFile = newEntry.getFile();
409     if (newFile != null && getFile() != null)
410     {
411       if (!newFile.equals(getFile()))
412       {
413         return false;
414       }
415       else
416       {
417         // files match.
418         if (!idMatches)
419         {
420           // this shouldn't happen, but could do if the id from the
421           // file is not the same as the id from the authority that provided
422           // the file
423           if (!newEntry.fakedPDBId() && !isAuthoritative())
424           {
425             return false;
426           } // otherwise we can update
427         }
428       }
429     }
430     else
431     {
432       // one has data, one doesn't ..
433       if (!idMatches)
434       {
435         return false;
436       } // otherwise maybe can update
437     }
438
439     /*
440      * Don't update if associated with different chains (ignoring case)
441      */
442     String newChain = newEntry.getChainCode();
443     if (newChain != null && newChain.length() > 0 && getChainCode() != null
444             && getChainCode().length() > 0
445             && !getChainCode().equalsIgnoreCase(newChain))
446     {
447       return false;
448     }
449
450     /*
451      * set file path if not already set
452      */
453     String newType = newEntry.getType();
454     if (getFile() == null && newFile != null)
455     {
456       setFile(newFile);
457       setType(newType);
458     }
459
460     /*
461      * set file type if new entry has it and we don't
462      * (for the case where file was not updated)
463      */
464     if (getType() == null && newType != null)
465     {
466       setType(newType);
467     }
468
469     /*
470      * set chain if not already set (we excluded differing 
471      * chains earlier) (ignoring case change only)
472      */
473     if (newChain != null && newChain.length() > 0
474             && !newChain.equalsIgnoreCase(getChainCode()))
475     {
476       setChainCode(newChain);
477     }
478
479     /*
480      * copy any new or modified properties
481      */
482     Enumeration<String> newProps = newEntry.getProperties();
483     while (newProps.hasMoreElements())
484     {
485       /*
486        * copy properties unless value matches; this defends against changing
487        * the case of chain_code which is wrapped in a CaseInsensitiveString
488        */
489       String key = newProps.nextElement();
490       Object value = newEntry.getProperty(key);
491       if (FAKED_ID.equals(key) || AUTHORITATIVE_ID.equals(key))
492       {
493         // we never update the fake ID property
494         continue;
495       }
496       if (!value.equals(getProperty(key)))
497       {
498         setProperty(key, value);
499       }
500     }
501     return true;
502   }
503
504   public void setAuthoritative(boolean isAuthoritative)
505   {
506     setProperty(AUTHORITATIVE_ID, Boolean.valueOf(isAuthoritative));
507   }
508
509   /**
510    * 
511    * @return true if the identifier should be preferred over any identifiers
512    *         embedded in the structure data
513    */
514   public boolean isAuthoritative()
515   {
516     if (_hasProperty(AUTHORITATIVE_ID))
517     {
518       return ((Boolean) getProperty(AUTHORITATIVE_ID));
519     }
520     return false;
521   }
522
523   /**
524    * set when Jalview has manufactured the ID using a local filename
525    * 
526    * @return
527    */
528   public boolean fakedPDBId()
529   {
530     if (_hasProperty(FAKED_ID))
531     {
532       return true;
533     }
534     return false;
535   }
536
537   public void setFakedPDBId(boolean faked)
538   {
539     if (faked)
540     {
541       setProperty(FAKED_ID, Boolean.TRUE);
542     }
543     else
544     {
545       if (properties != null)
546       {
547         properties.remove(FAKED_ID);
548       }
549     }
550   }
551
552   private boolean _hasProperty(final String key)
553   {
554     return (properties != null && properties.containsKey(key));
555   }
556
557   private static final String RETRIEVE_FROM = "RETRIEVE_FROM";
558
559   private static final String PROVIDER = "PROVIDER";
560
561   private static final String MODELPAGE = "PROVIDERPAGE";
562
563   /**
564    * Permanent URI for retrieving the original structure data
565    * 
566    * @param urlStr
567    */
568   public void setRetrievalUrl(String urlStr)
569   {
570     setProperty(RETRIEVE_FROM, urlStr);
571   }
572
573   public boolean hasRetrievalUrl()
574   {
575     return _hasProperty(RETRIEVE_FROM);
576   }
577
578   /**
579    * get the Permanent URI for retrieving the original structure data
580    */
581   public String getRetrievalUrl()
582   {
583     return (String) getProperty(RETRIEVE_FROM);
584   }
585
586   /**
587    * Data provider name - from 3D Beacons
588    * 
589    * @param provider
590    */
591   public void setProvider(String provider)
592   {
593     setProperty(PROVIDER, provider);
594   }
595
596   /**
597    * Get Data provider name - from 3D Beacons
598    * 
599    */
600   public String getProvider()
601   {
602     return (String) getProperty(PROVIDER);
603   }
604
605   /**
606    * Permanent URI for retrieving the original structure data
607    * 
608    * @param urlStr
609    */
610   public void setProviderPage(String urlStr)
611   {
612     setProperty(MODELPAGE, urlStr);
613   }
614
615   /**
616    * get the Permanent URI for retrieving the original structure data
617    */
618   public String getProviderPage()
619   {
620     return (String) getProperty(MODELPAGE);
621   }
622
623   public boolean hasProviderPage()
624   {
625     return _hasProperty(MODELPAGE);
626   }
627
628   public boolean hasProvider()
629   {
630     return _hasProperty(PROVIDER);
631   }
632
633   public StructureFile getStructureFile()
634   {
635     return sf;
636   }
637
638   public void setStructureFile(StructureFile f)
639   {
640     sf = f;
641   }
642
643   public boolean hasStructureFile()
644   {
645     return sf != null && sf.inFile != null && sf.inFile.exists();
646   }
647 }