1 /* 2 * Jalview - A Sequence Alignment Editor and Viewer (2.11.1.4) 3 * Copyright (C) 2021 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 String file; 42 43 private String type; 44 45 private String id; 46 47 public enum Type 48 { 49 // TODO is FILE needed; if not is this enum needed, or can we 50 // use FileFormatI for PDB, MMCIF? 51 PDB("pdb", "pdb"), MMCIF("mmcif", "cif"), FILE("?", "?"); 52 53 /* 54 * file extension for cached structure file; must be one that 55 * is recognised by Chimera 'open' command 56 * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html 57 */ 58 String ext; 59 60 /* 61 * format specifier used in dbfetch request 62 * @see http://www.ebi.ac.uk/Tools/dbfetch/dbfetch/dbfetch.databases#pdb 63 */ 64 String format; 65 Type(String fmt, String ex)66 private Type(String fmt, String ex) 67 { 68 format = fmt; 69 ext = ex; 70 } 71 getFormat()72 public String getFormat() 73 { 74 return format; 75 } 76 getExtension()77 public String getExtension() 78 { 79 return ext; 80 } 81 82 /** 83 * case insensitive matching for Type enum 84 * 85 * @param value 86 * @return 87 */ getType(String value)88 public static Type getType(String value) 89 { 90 for (Type t : Type.values()) 91 { 92 if (t.toString().equalsIgnoreCase(value)) 93 { 94 return t; 95 } 96 } 97 return null; 98 } 99 100 /** 101 * case insensitive equivalence for strings resolving to PDBEntry type 102 * 103 * @param t 104 * @return 105 */ matches(String t)106 public boolean matches(String t) 107 { 108 return (this.toString().equalsIgnoreCase(t)); 109 } 110 } 111 112 /** 113 * Answers true if obj is a PDBEntry with the same id and chain code (both 114 * ignoring case), file, type and properties 115 */ 116 @Override equals(Object obj)117 public boolean equals(Object obj) 118 { 119 if (obj == null || !(obj instanceof PDBEntry)) 120 { 121 return false; 122 } 123 if (obj == this) 124 { 125 return true; 126 } 127 PDBEntry o = (PDBEntry) obj; 128 129 /* 130 * note that chain code is stored as a property wrapped by a 131 * CaseInsensitiveString, so we are in effect doing a 132 * case-insensitive comparison of chain codes 133 */ 134 boolean idMatches = id == o.id 135 || (id != null && id.equalsIgnoreCase(o.id)); 136 boolean fileMatches = file == o.file 137 || (file != null && file.equals(o.file)); 138 boolean typeMatches = type == o.type 139 || (type != null && type.equals(o.type)); 140 if (idMatches && fileMatches && typeMatches) 141 { 142 return properties == o.properties 143 || (properties != null && properties.equals(o.properties)); 144 } 145 return false; 146 } 147 148 /** 149 * Default constructor 150 */ PDBEntry()151 public PDBEntry() 152 { 153 } 154 PDBEntry(String pdbId, String chain, PDBEntry.Type type, String filePath)155 public PDBEntry(String pdbId, String chain, PDBEntry.Type type, 156 String filePath) 157 { 158 init(pdbId, chain, type, filePath); 159 } 160 161 /** 162 * @param pdbId 163 * @param chain 164 * @param entryType 165 * @param filePath 166 */ init(String pdbId, String chain, PDBEntry.Type entryType, String filePath)167 void init(String pdbId, String chain, PDBEntry.Type entryType, 168 String filePath) 169 { 170 this.id = pdbId; 171 this.type = entryType == null ? null : entryType.toString(); 172 this.file = filePath; 173 setChainCode(chain); 174 } 175 176 /** 177 * Copy constructor. 178 * 179 * @param entry 180 */ PDBEntry(PDBEntry entry)181 public PDBEntry(PDBEntry entry) 182 { 183 file = entry.file; 184 type = entry.type; 185 id = entry.id; 186 if (entry.properties != null) 187 { 188 properties = (Hashtable<String, Object>) entry.properties.clone(); 189 } 190 } 191 192 /** 193 * Make a PDBEntry from a DBRefEntry. The accession code is used for the PDB 194 * id, but if it is 5 characters in length, the last character is removed and 195 * set as the chain code instead. 196 * 197 * @param dbr 198 */ PDBEntry(DBRefEntry dbr)199 public PDBEntry(DBRefEntry dbr) 200 { 201 if (!DBRefSource.PDB.equals(dbr.getSource())) 202 { 203 throw new IllegalArgumentException( 204 "Invalid source: " + dbr.getSource()); 205 } 206 207 String pdbId = dbr.getAccessionId(); 208 String chainCode = null; 209 if (pdbId.length() == PDB_ID_LENGTH + 1) 210 { 211 char chain = pdbId.charAt(PDB_ID_LENGTH); 212 if (('a' <= chain && chain <= 'z') || ('A' <= chain && chain <= 'Z')) 213 { 214 pdbId = pdbId.substring(0, PDB_ID_LENGTH); 215 chainCode = String.valueOf(chain); 216 } 217 } 218 init(pdbId, chainCode, null, null); 219 } 220 setFile(String f)221 public void setFile(String f) 222 { 223 this.file = f; 224 } 225 getFile()226 public String getFile() 227 { 228 return file; 229 } 230 setType(String t)231 public void setType(String t) 232 { 233 this.type = t; 234 } 235 setType(PDBEntry.Type type)236 public void setType(PDBEntry.Type type) 237 { 238 this.type = type == null ? null : type.toString(); 239 } 240 getType()241 public String getType() 242 { 243 return type; 244 } 245 setId(String id)246 public void setId(String id) 247 { 248 this.id = id; 249 } 250 getId()251 public String getId() 252 { 253 return id; 254 } 255 setProperty(String key, Object value)256 public void setProperty(String key, Object value) 257 { 258 if (this.properties == null) 259 { 260 this.properties = new Hashtable<String, Object>(); 261 } 262 properties.put(key, value); 263 } 264 getProperty(String key)265 public Object getProperty(String key) 266 { 267 return properties == null ? null : properties.get(key); 268 } 269 270 /** 271 * Returns an enumeration of the keys of this object's properties (or an empty 272 * enumeration if it has no properties) 273 * 274 * @return 275 */ getProperties()276 public Enumeration<String> getProperties() 277 { 278 if (properties == null) 279 { 280 return Collections.emptyEnumeration(); 281 } 282 return properties.keys(); 283 } 284 285 /** 286 * 287 * @return null or a string for associated chain IDs 288 */ getChainCode()289 public String getChainCode() 290 { 291 return (properties == null || properties.get(CHAIN_ID) == null) ? null 292 : properties.get(CHAIN_ID).toString(); 293 } 294 295 /** 296 * Sets a non-case-sensitive property for the given chain code. Two PDBEntry 297 * objects which differ only in the case of their chain code are considered 298 * equal. This avoids duplication of objects in lists of PDB ids. 299 * 300 * @param chainCode 301 */ setChainCode(String chainCode)302 public void setChainCode(String chainCode) 303 { 304 if (chainCode == null) 305 { 306 deleteProperty(CHAIN_ID); 307 } 308 else 309 { 310 setProperty(CHAIN_ID, new CaseInsensitiveString(chainCode)); 311 } 312 } 313 314 /** 315 * Deletes the property with the given key, and returns the deleted value (or 316 * null) 317 */ deleteProperty(String key)318 Object deleteProperty(String key) 319 { 320 Object result = null; 321 if (properties != null) 322 { 323 result = properties.remove(key); 324 } 325 return result; 326 } 327 328 @Override toString()329 public String toString() 330 { 331 return id; 332 } 333 334 /** 335 * Getter provided for Castor binding only. Application code should call 336 * getProperty() or getProperties() instead. 337 * 338 * @deprecated 339 * @see #getProperty(String) 340 * @see #getProperties() 341 * @see jalview.ws.dbsources.Uniprot#getUniprotEntries 342 * @return 343 */ 344 @Deprecated getProps()345 public Hashtable<String, Object> getProps() 346 { 347 return properties; 348 } 349 350 /** 351 * Setter provided for Castor binding only. Application code should call 352 * setProperty() instead. 353 * 354 * @deprecated 355 * @return 356 */ 357 @Deprecated setProps(Hashtable<String, Object> props)358 public void setProps(Hashtable<String, Object> props) 359 { 360 properties = props; 361 } 362 363 /** 364 * Answers true if this object is either equivalent to, or can be 'improved' 365 * by, the given entry. 366 * <p> 367 * If newEntry has the same id (ignoring case), and doesn't have a conflicting 368 * file spec or chain code, then update this entry from its file and/or chain 369 * code. 370 * 371 * @param newEntry 372 * @return true if modifications were made 373 */ updateFrom(PDBEntry newEntry)374 public boolean updateFrom(PDBEntry newEntry) 375 { 376 if (this.equals(newEntry)) 377 { 378 return true; 379 } 380 381 String newId = newEntry.getId(); 382 if (newId == null || getId() == null) 383 { 384 return false; // shouldn't happen 385 } 386 387 /* 388 * id has to match (ignoring case) 389 */ 390 if (!getId().equalsIgnoreCase(newId)) 391 { 392 return false; 393 } 394 395 /* 396 * Don't update if associated with different structure files 397 */ 398 String newFile = newEntry.getFile(); 399 if (newFile != null && getFile() != null && !newFile.equals(getFile())) 400 { 401 return false; 402 } 403 404 /* 405 * Don't update if associated with different chains (ignoring case) 406 */ 407 String newChain = newEntry.getChainCode(); 408 if (newChain != null && newChain.length() > 0 && getChainCode() != null 409 && getChainCode().length() > 0 410 && !getChainCode().equalsIgnoreCase(newChain)) 411 { 412 return false; 413 } 414 415 /* 416 * set file path if not already set 417 */ 418 String newType = newEntry.getType(); 419 if (getFile() == null && newFile != null) 420 { 421 setFile(newFile); 422 setType(newType); 423 } 424 425 /* 426 * set file type if new entry has it and we don't 427 * (for the case where file was not updated) 428 */ 429 if (getType() == null && newType != null) 430 { 431 setType(newType); 432 } 433 434 /* 435 * set chain if not already set (we excluded differing 436 * chains earlier) (ignoring case change only) 437 */ 438 if (newChain != null && newChain.length() > 0 439 && !newChain.equalsIgnoreCase(getChainCode())) 440 { 441 setChainCode(newChain); 442 } 443 444 /* 445 * copy any new or modified properties 446 */ 447 Enumeration<String> newProps = newEntry.getProperties(); 448 while (newProps.hasMoreElements()) 449 { 450 /* 451 * copy properties unless value matches; this defends against changing 452 * the case of chain_code which is wrapped in a CaseInsensitiveString 453 */ 454 String key = newProps.nextElement(); 455 Object value = newEntry.getProperty(key); 456 if (!value.equals(getProperty(key))) 457 { 458 setProperty(key, value); 459 } 460 } 461 return true; 462 } 463 } 464