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