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