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