adcefb3826530d567d1b9de69a3d7ee8a404d8b5
[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   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   public PDBEntry(String pdbId, String chain, PDBEntry.Type type,
158           String filePath)
159   {
160     init(pdbId, chain, type, filePath);
161   }
162
163   /**
164    * @param pdbId
165    * @param chain
166    * @param entryType
167    * @param filePath
168    */
169   void init(String pdbId, String chain, PDBEntry.Type entryType,
170           String filePath)
171   {
172     this.id = pdbId;
173     this.type = entryType == null ? null : entryType.toString();
174     this.file = filePath;
175     setChainCode(chain);
176   }
177
178   /**
179    * Copy constructor.
180    * 
181    * @param entry
182    */
183   public PDBEntry(PDBEntry entry)
184   {
185     file = entry.file;
186     type = entry.type;
187     id = entry.id;
188     if (entry.properties != null)
189     {
190       properties = (Hashtable<String, Object>) entry.properties.clone();
191     }
192   }
193
194   /**
195    * Make a PDBEntry from a DBRefEntry. The accession code is used for the PDB
196    * id, but if it is 5 characters in length, the last character is removed and
197    * set as the chain code instead.
198    * 
199    * @param dbr
200    */
201   public PDBEntry(DBRefEntry dbr)
202   {
203     if (!DBRefSource.PDB.equals(dbr.getSource()))
204     {
205       throw new IllegalArgumentException(
206               "Invalid source: " + dbr.getSource());
207     }
208
209     String pdbId = dbr.getAccessionId();
210     String chainCode = null;
211     if (pdbId.length() == PDB_ID_LENGTH + 1)
212     {
213       char chain = pdbId.charAt(PDB_ID_LENGTH);
214       if (('a' <= chain && chain <= 'z') || ('A' <= chain && chain <= 'Z'))
215       {
216         pdbId = pdbId.substring(0, PDB_ID_LENGTH);
217         chainCode = String.valueOf(chain);
218       }
219     }
220     init(pdbId, chainCode, null, null);
221   }
222
223   public void setFile(String f)
224   {
225     this.file = f;
226   }
227
228   public String getFile()
229   {
230     return file;
231   }
232
233   public void setType(String t)
234   {
235     this.type = t;
236   }
237
238   public void setType(PDBEntry.Type type)
239   {
240     this.type = type == null ? null : type.toString();
241   }
242
243   public String getType()
244   {
245     return type;
246   }
247
248   public void setId(String id)
249   {
250     this.id = id;
251   }
252
253   public String getId()
254   {
255     return id;
256   }
257
258   public void setProperty(String key, Object value)
259   {
260     if (this.properties == null)
261     {
262       this.properties = new Hashtable<String, Object>();
263     }
264     properties.put(key, value);
265   }
266
267   public Object getProperty(String key)
268   {
269     return properties == null ? null : properties.get(key);
270   }
271
272   /**
273    * Returns an enumeration of the keys of this object's properties (or an empty
274    * enumeration if it has no properties)
275    * 
276    * @return
277    */
278   public Enumeration<String> getProperties()
279   {
280     if (properties == null)
281     {
282       return Collections.emptyEnumeration();
283     }
284     return properties.keys();
285   }
286
287   /**
288    * 
289    * @return null or a string for associated chain IDs
290    */
291   public String getChainCode()
292   {
293     return (properties == null || properties.get(CHAIN_ID) == null) ? null
294             : properties.get(CHAIN_ID).toString();
295   }
296
297   /**
298    * Sets a non-case-sensitive property for the given chain code. Two PDBEntry
299    * objects which differ only in the case of their chain code are considered
300    * equal. This avoids duplication of objects in lists of PDB ids.
301    * 
302    * @param chainCode
303    */
304   public void setChainCode(String chainCode)
305   {
306     if (chainCode == null)
307     {
308       deleteProperty(CHAIN_ID);
309     }
310     else
311     {
312       setProperty(CHAIN_ID, new CaseInsensitiveString(chainCode));
313     }
314   }
315
316   /**
317    * Deletes the property with the given key, and returns the deleted value (or
318    * null)
319    */
320   Object deleteProperty(String key)
321   {
322     Object result = null;
323     if (properties != null)
324     {
325       result = properties.remove(key);
326     }
327     return result;
328   }
329
330   @Override
331   public String toString()
332   {
333     return id;
334   }
335
336   /**
337    * Getter provided for Castor binding only. Application code should call
338    * getProperty() or getProperties() instead.
339    * 
340    * @deprecated
341    * @see #getProperty(String)
342    * @see #getProperties()
343    * @see jalview.ws.dbsources.Uniprot#getUniprotEntries
344    * @return
345    */
346   @Deprecated
347   public Hashtable<String, Object> getProps()
348   {
349     return properties;
350   }
351
352   /**
353    * Setter provided for Castor binding only. Application code should call
354    * setProperty() instead.
355    * 
356    * @deprecated
357    * @return
358    */
359   @Deprecated
360   public void setProps(Hashtable<String, Object> props)
361   {
362     properties = props;
363   }
364
365   /**
366    * Answers true if this object is either equivalent to, or can be 'improved'
367    * by, the given entry.
368    * <p>
369    * If newEntry has the same id (ignoring case), and doesn't have a conflicting
370    * file spec or chain code, then update this entry from its file and/or chain
371    * code.
372    * 
373    * @param newEntry
374    * @return true if modifications were made
375    */
376   public boolean updateFrom(PDBEntry newEntry)
377   {
378     if (this.equals(newEntry))
379     {
380       return true;
381     }
382
383     String newId = newEntry.getId();
384     if (newId == null || getId() == null)
385     {
386       return false; // shouldn't happen
387     }
388
389     boolean idMatches = getId().equalsIgnoreCase(newId);
390
391     /*
392      * Don't update if associated with different structure files
393      */
394     String newFile = newEntry.getFile();
395     if (newFile != null && getFile() != null)
396     {
397       if (!newFile.equals(getFile()))
398       {
399         return false;
400       }
401       else
402       {
403         // files match.
404         if (!idMatches)
405         {
406           // this shouldn't happen, but could do if the id from the
407           // file is not the same as the id from the authority that provided
408           // the file
409           if (!newEntry.fakedPDBId())
410           {
411             return false;
412           } // otherwise we can update
413         }
414       }
415     }
416     else
417     {
418       // one has data, one doesn't ..
419       if (!idMatches)
420       {
421         return false;
422       } // otherwise maybe can update
423     }
424
425     /*
426      * Don't update if associated with different chains (ignoring case)
427      */
428     String newChain = newEntry.getChainCode();
429     if (newChain != null && newChain.length() > 0 && getChainCode() != null
430             && getChainCode().length() > 0
431             && !getChainCode().equalsIgnoreCase(newChain))
432     {
433       return false;
434     }
435
436     /*
437      * set file path if not already set
438      */
439     String newType = newEntry.getType();
440     if (getFile() == null && newFile != null)
441     {
442       setFile(newFile);
443       setType(newType);
444     }
445
446     /*
447      * set file type if new entry has it and we don't
448      * (for the case where file was not updated)
449      */
450     if (getType() == null && newType != null)
451     {
452       setType(newType);
453     }
454
455     /*
456      * set chain if not already set (we excluded differing 
457      * chains earlier) (ignoring case change only)
458      */
459     if (newChain != null && newChain.length() > 0
460             && !newChain.equalsIgnoreCase(getChainCode()))
461     {
462       setChainCode(newChain);
463     }
464
465     /*
466      * copy any new or modified properties
467      */
468     Enumeration<String> newProps = newEntry.getProperties();
469     while (newProps.hasMoreElements())
470     {
471       /*
472        * copy properties unless value matches; this defends against changing
473        * the case of chain_code which is wrapped in a CaseInsensitiveString
474        */
475       String key = newProps.nextElement();
476       Object value = newEntry.getProperty(key);
477       if (FAKED_ID.equals(key))
478       {
479         // we never update the fake ID property
480         continue;
481       }
482       if (!value.equals(getProperty(key)))
483       {
484         setProperty(key, value);
485       }
486     }
487     return true;
488   }
489   
490   /**
491    * set when Jalview has manufactured the ID using a local filename
492    * @return
493    */
494   public boolean fakedPDBId()
495   {
496     if (_hasProperty(FAKED_ID))
497     {
498       return true;
499     }
500     return false;
501   }
502   public void setFakedPDBId(boolean faked)
503   {
504     if (faked)
505     {
506       setProperty(FAKED_ID, Boolean.TRUE);
507     }
508     else 
509     {
510       if (properties!=null) {
511         properties.remove(FAKED_ID);
512       }
513     }
514   }
515
516   private boolean _hasProperty(final String key)
517   {
518     return (properties != null && properties.containsKey(key));
519   }
520
521   private static final String RETRIEVE_FROM = "RETRIEVE_FROM";
522
523   private static final String PROVIDER = "PROVIDER";
524
525   private static final String MODELPAGE = "PROVIDERPAGE";
526
527   /**
528    * Permanent URI for retrieving the original structure data
529    * 
530    * @param urlStr
531    */
532   public void setRetrievalUrl(String urlStr)
533   {
534     setProperty(RETRIEVE_FROM, urlStr);
535   }
536
537   public boolean hasRetrievalUrl()
538   {
539     return _hasProperty(RETRIEVE_FROM);
540   }
541
542   /**
543    * get the Permanent URI for retrieving the original structure data
544    */
545   public String getRetrievalUrl()
546   {
547     return (String) getProperty(RETRIEVE_FROM);
548   }
549
550   /**
551    * Data provider name - from 3D Beacons
552    * 
553    * @param provider
554    */
555   public void setProvider(String provider)
556   {
557     setProperty(PROVIDER, provider);
558   }
559
560   /**
561    * Get Data provider name - from 3D Beacons
562    * 
563    */
564   public String getProvider()
565   {
566     return (String) getProperty(PROVIDER);
567   }
568
569   /**
570    * Permanent URI for retrieving the original structure data
571    * 
572    * @param urlStr
573    */
574   public void setProviderPage(String urlStr)
575   {
576     setProperty(MODELPAGE, urlStr);
577   }
578
579   /**
580    * get the Permanent URI for retrieving the original structure data
581    */
582   public String getProviderPage()
583   {
584     return (String) getProperty(MODELPAGE);
585   }
586
587   public boolean hasProviderPage()
588   {
589     return _hasProperty(MODELPAGE);
590   }
591
592   public boolean hasProvider()
593   {
594     return _hasProperty(PROVIDER);
595   }
596 }