c205e9ec1965da8237a330405d87ce92f284641a
[jalview.git] / src / jalview / ws / sifts / SiftsClient.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.ws.sifts;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.api.DBRefEntryI;
25 import jalview.api.SiftsClientI;
26 import jalview.datamodel.DBRefEntry;
27 import jalview.datamodel.DBRefSource;
28 import jalview.datamodel.SequenceI;
29 import jalview.schemes.ResidueProperties;
30 import jalview.structure.StructureMapping;
31 import jalview.util.Format;
32 import jalview.xml.binding.sifts.Entry;
33 import jalview.xml.binding.sifts.Entry.Entity;
34 import jalview.xml.binding.sifts.Entry.Entity.Segment;
35 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListMapRegion.MapRegion;
36 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue;
37 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.CrossRefDb;
38 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.ResidueDetail;
39 import jalview.xml.binding.sifts.Entry.ListDB.Db;
40
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.PrintStream;
48 import java.net.URL;
49 import java.net.URLConnection;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Collection;
53 import java.util.HashMap;
54 import java.util.HashSet;
55 import java.util.List;
56 import java.util.TreeMap;
57 import java.util.zip.GZIPInputStream;
58
59 import javax.xml.bind.JAXBContext;
60 import javax.xml.bind.JAXBException;
61 import javax.xml.bind.Unmarshaller;
62 import javax.xml.stream.FactoryConfigurationError;
63 import javax.xml.stream.XMLInputFactory;
64 import javax.xml.stream.XMLStreamException;
65 import javax.xml.stream.XMLStreamReader;
66
67 import MCview.Atom;
68 import MCview.PDBChain;
69 import MCview.PDBfile;
70
71 public class SiftsClient implements SiftsClientI
72 {
73   private Entry siftsEntry;
74
75   private PDBfile pdb;
76
77   private String pdbId;
78
79   private String structId;
80
81   private String segStartEnd;
82
83   private CoordinateSys seqCoordSys = CoordinateSys.UNIPROT;
84
85   private static final int BUFFER_SIZE = 4096;
86
87   public static final int UNASSIGNED = -1;
88
89   private static final int PDB_RES_POS = 0;
90
91   private static final int PDB_ATOM_POS = 1;
92
93   private static final String NOT_FOUND = "Not_Found";
94
95   private static final String NOT_OBSERVED = "Not_Observed";
96
97   private static final String SIFTS_FTP_BASE_URL = "ftp://ftp.ebi.ac.uk/pub/databases/msd/sifts/xml/";
98
99   public static final String DEFAULT_SIFTS_DOWNLOAD_DIR = System
100           .getProperty("user.home")
101           + File.separatorChar
102           + ".sifts_downloads" + File.separatorChar;
103
104   public static final String SIFTS_DOWNLOAD_DIR = jalview.bin.Cache
105           .getDefault("sifts_download_dir", DEFAULT_SIFTS_DOWNLOAD_DIR);
106
107   private final static String NEWLINE = System.lineSeparator();
108
109   private String curSourceDBRef;
110
111   private HashSet<String> curDBRefAccessionIdsString;
112
113   public enum CoordinateSys
114   {
115     UNIPROT("UniProt"), PDB("PDBresnum"), PDBe("PDBe");
116     private String name;
117
118     private CoordinateSys(String name)
119     {
120       this.name = name;
121     }
122
123     public String getName()
124     {
125       return name;
126     }
127   };
128
129   public enum ResidueDetailType
130   {
131     NAME_SEC_STRUCTURE("nameSecondaryStructure"), CODE_SEC_STRUCTURE(
132             "codeSecondaryStructure"), ANNOTATION("Annotation");
133     private String code;
134
135     private ResidueDetailType(String code)
136     {
137       this.code = code;
138     }
139
140     public String getCode()
141     {
142       return code;
143     }
144   };
145
146   /**
147    * Fetch SIFTs file for the given PDBfile and construct an instance of
148    * SiftsClient
149    * 
150    * @param pdbId
151    * @throws SiftsException
152    */
153   public SiftsClient(PDBfile pdb) throws SiftsException
154   {
155     this.pdb = pdb;
156     this.pdbId = pdb.id;
157     File siftsFile = getSiftsFile(pdbId);
158     siftsEntry = parseSIFTs(siftsFile);
159   }
160
161   /**
162    * Construct an instance of SiftsClient using the supplied SIFTs file. Note:
163    * The SIFTs file should correspond to the PDB Id in PDBfile instance
164    * 
165    * @param pdbId
166    * @param siftsFile
167    * @throws SiftsException
168    * @throws Exception
169    */
170   public SiftsClient(PDBfile pdb, File siftsFile) throws SiftsException
171   {
172     this.pdb = pdb;
173     this.pdbId = pdb.id;
174     siftsEntry = parseSIFTs(siftsFile);
175   }
176
177   /**
178    * Parse the given SIFTs File and return a JAXB POJO of parsed data
179    * 
180    * @param siftFile
181    *          - the GZipped SIFTs XML file to parse
182    * @return
183    * @throws Exception
184    *           if a problem occurs while parsing the SIFTs XML
185    */
186   private Entry parseSIFTs(File siftFile) throws SiftsException
187   {
188     try (InputStream in = new FileInputStream(siftFile);
189             GZIPInputStream gzis = new GZIPInputStream(in);)
190     {
191       System.out.println("File : " + siftFile.getAbsolutePath());
192       JAXBContext jc = JAXBContext.newInstance("jalview.xml.binding.sifts");
193       XMLStreamReader streamReader = XMLInputFactory.newInstance()
194               .createXMLStreamReader(gzis);
195       Unmarshaller um = jc.createUnmarshaller();
196       return (Entry) um.unmarshal(streamReader);
197     } catch (JAXBException e)
198     {
199       e.printStackTrace();
200       throw new SiftsException(e.getMessage());
201     } catch (FileNotFoundException e)
202     {
203       e.printStackTrace();
204       throw new SiftsException(e.getMessage());
205     } catch (XMLStreamException e)
206     {
207       e.printStackTrace();
208       throw new SiftsException(e.getMessage());
209     } catch (FactoryConfigurationError e)
210     {
211       e.printStackTrace();
212       throw new SiftsException(e.getMessage());
213     } catch (IOException e)
214     {
215       e.printStackTrace();
216       throw new SiftsException(e.getMessage());
217     }
218   }
219
220   /**
221    * Get a SIFTs XML file for a given PDB Id from Cache or download from FTP
222    * repository if not found in cache
223    * 
224    * @param pdbId
225    * @return SIFTs XML file
226    * @throws SiftsException
227    */
228   public static File getSiftsFile(String pdbId) throws SiftsException
229   {
230     File siftsFile = new File(SIFTS_DOWNLOAD_DIR + pdbId.toLowerCase()
231             + ".xml.gz");
232     if (siftsFile.exists())
233     {
234       // TODO it may be worth performing an age check to determine if a
235       // new SIFTs file should be re-downloaded as SIFTs entries are usually
236       // updated weekly
237       System.out.println(">>> SIFTS File already downloaded for " + pdbId);
238       return siftsFile;
239     }
240     siftsFile = downloadSiftsFile(pdbId.toLowerCase());
241     return siftsFile;
242   }
243
244   /**
245    * Download a SIFTs XML file for a given PDB Id from an FTP repository
246    * 
247    * @param pdbId
248    * @return downloaded SIFTs XML file
249    * @throws SiftsException
250    */
251   public static File downloadSiftsFile(String pdbId) throws SiftsException
252   {
253     String siftFile = pdbId + ".xml.gz";
254     String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
255     String downloadedSiftsFile = SIFTS_DOWNLOAD_DIR + siftFile;
256     File siftsDownloadDir = new File(SIFTS_DOWNLOAD_DIR);
257     if (!siftsDownloadDir.exists())
258     {
259       siftsDownloadDir.mkdirs();
260     }
261     try
262     {
263       System.out.println(">> Download ftp url : " + siftsFileFTPURL);
264       URL url = new URL(siftsFileFTPURL);
265       URLConnection conn = url.openConnection();
266       InputStream inputStream = conn.getInputStream();
267       FileOutputStream outputStream = new FileOutputStream(
268               downloadedSiftsFile);
269       byte[] buffer = new byte[BUFFER_SIZE];
270       int bytesRead = -1;
271       while ((bytesRead = inputStream.read(buffer)) != -1)
272       {
273         outputStream.write(buffer, 0, bytesRead);
274       }
275       outputStream.close();
276       inputStream.close();
277       System.out.println(">>> File downloaded : " + downloadedSiftsFile);
278     } catch (IOException ex)
279     {
280       throw new SiftsException(ex.getMessage());
281     }
282     return new File(downloadedSiftsFile);
283   }
284
285   /**
286    * Delete the SIFTs file for the given PDB Id in the local SIFTs download
287    * directory
288    * 
289    * @param pdbId
290    * @return true if the file was deleted or doesn't exist
291    */
292   public static boolean deleteSiftsFileByPDBId(String pdbId)
293   {
294     File siftsFile = new File(SIFTS_DOWNLOAD_DIR + pdbId.toLowerCase()
295             + ".xml.gz");
296     if (siftsFile.exists())
297     {
298       return siftsFile.delete();
299     }
300     return true;
301   }
302
303
304   /**
305    * Get a valid SIFTs DBRef for the given sequence current SIFTs entry
306    * 
307    * @param seq
308    *          - the target sequence for the operation
309    * @return a valid DBRefEntry that is SIFTs compatible
310    * @throws Exception
311    *           if no valid source DBRefEntry was found for the given sequences
312    */
313   public DBRefEntryI getValidSourceDBRef(SequenceI seq)
314           throws SiftsException
315   {
316     DBRefEntryI sourceDBRef = null;
317     sourceDBRef = seq.getSourceDBRef();
318     if (sourceDBRef != null && isValidDBRefEntry(sourceDBRef))
319     {
320       return sourceDBRef;
321     }
322     else
323     {
324       DBRefEntry[] dbRefs = seq.getDBRefs();
325       if (dbRefs == null || dbRefs.length < 1)
326       {
327         // final SequenceI[] seqs = new SequenceI[] { seq };
328         // new jalview.ws.DBRefFetcher(seqs, null, null, null, false)
329         // .fetchDBRefs(true);
330         // dbRefs = seq.getDBRefs();
331       }
332
333       if (dbRefs == null || dbRefs.length < 1)
334       {
335         throw new SiftsException("Could not get source DB Ref");
336       }
337
338       for (DBRefEntryI dbRef : dbRefs)
339       {
340         if (dbRef == null || dbRef.getAccessionId() == null
341                 || dbRef.getSource() == null)
342         {
343           continue;
344         }
345         if (isFoundInSiftsEntry(dbRef.getAccessionId())
346                 && (dbRef.getSource().equalsIgnoreCase(DBRefSource.UNIPROT) || dbRef
347                         .getSource().equalsIgnoreCase(DBRefSource.PDB)))
348         {
349           return dbRef;
350         }
351       }
352     }
353     if (sourceDBRef != null && isValidDBRefEntry(sourceDBRef))
354     {
355       return sourceDBRef;
356     }
357     throw new SiftsException("Could not get source DB Ref");
358   }
359
360
361   /**
362    * Check that the DBRef Entry is properly populated and is available in this
363    * SiftClient instance
364    * 
365    * @param entry
366    *          - DBRefEntry to validate
367    * @return true validation is successful otherwise false is returned.
368    */
369   private boolean isValidDBRefEntry(DBRefEntryI entry)
370   {
371     return entry != null && entry.getAccessionId() != null
372             && isFoundInSiftsEntry(entry.getAccessionId());
373   }
374
375   @Override
376   public HashSet<String> getAllMappingAccession()
377   {
378     HashSet<String> accessions = new HashSet<String>();
379     List<Entity> entities = siftsEntry.getEntity();
380     for (Entity entity : entities)
381     {
382       List<Segment> segments = entity.getSegment();
383       for (Segment segment : segments)
384       {
385         List<MapRegion> mapRegions = segment.getListMapRegion()
386                 .getMapRegion();
387         for (MapRegion mapRegion : mapRegions)
388         {
389           accessions.add(mapRegion.getDb().getDbAccessionId());
390         }
391       }
392     }
393     return accessions;
394   }
395
396   @Override
397   public StructureMapping getSiftsStructureMapping(SequenceI seq,
398           String pdbFile, String chain) throws SiftsException
399   {
400     structId = (chain == null) ? pdbId : pdbId + "|" + chain;
401     System.out.println("Getting mapping for: " + pdbId + "|" + chain
402             + " : seq- " + seq.getName());
403
404     final StringBuilder mappingDetails = new StringBuilder(128);
405     PrintStream ps = new PrintStream(System.out)
406     {
407       @Override
408       public void print(String x)
409       {
410         mappingDetails.append(x);
411       }
412
413       @Override
414       public void println()
415       {
416         mappingDetails.append(NEWLINE);
417       }
418     };
419     HashMap<Integer, int[]> mapping = getGreedyMapping(chain, seq, ps);
420
421     String mappingOutput = mappingDetails.toString();
422     StructureMapping siftsMapping = new StructureMapping(seq, pdbFile,
423             pdbId, chain, mapping,
424             mappingOutput);
425     return siftsMapping;
426   }
427
428   @Override
429   public HashMap<Integer, int[]> getGreedyMapping(String entityId, SequenceI seq,
430           java.io.PrintStream os)
431  throws SiftsException
432   {
433     ArrayList<Integer> omitNonObserved = new ArrayList<Integer>();
434     int nonObservedShiftIndex = 0;
435     System.out.println("Generating mappings for : " + entityId);
436     Entity entity = null;
437     entity = getEntityById(entityId);
438     String originalSeq = AlignSeq.extractGaps(
439             jalview.util.Comparison.GapChars,
440             seq.getSequenceAsString());
441     HashMap<Integer, int[]> mapping = new HashMap<Integer, int[]>();
442     DBRefEntryI sourceDBRef = seq.getSourceDBRef();
443     if (sourceDBRef == null)
444     {
445       sourceDBRef = getValidSourceDBRef(seq);
446       // TODO ensure sequence start/end is in the same coordinate system and
447       // consistent with the choosen sourceDBRef
448     }
449
450     // set sequence coordinate system - default value is UniProt
451     if (sourceDBRef.getSource().equalsIgnoreCase(DBRefSource.PDB))
452     {
453       seqCoordSys = CoordinateSys.PDB;
454     }
455
456     HashSet<String> dbRefAccessionIdsString = new HashSet<String>();
457     for (DBRefEntry dbref : seq.getDBRefs())
458     {
459       dbRefAccessionIdsString.add(dbref.getAccessionId().toLowerCase());
460     }
461     dbRefAccessionIdsString.add(sourceDBRef.getAccessionId().toLowerCase());
462
463     curDBRefAccessionIdsString = dbRefAccessionIdsString;
464     curSourceDBRef = sourceDBRef.getAccessionId();
465
466     TreeMap<Integer, String> resNumMap = new TreeMap<Integer, String>();
467     List<Segment> segments = entity.getSegment();
468     for (Segment segment : segments)
469     {
470       segStartEnd = segment.getStart() + " - " + segment.getEnd();
471       System.out.println("Mappging segments : " + segment.getSegId() + "\\"
472               + segStartEnd);
473       List<Residue> residues = segment.getListResidue().getResidue();
474       for (Residue residue : residues)
475       {
476         int currSeqIndex = UNASSIGNED;
477         List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
478         CrossRefDb pdbRefDb = null;
479         for (CrossRefDb cRefDb : cRefDbs)
480         {
481           if (cRefDb.getDbSource().equalsIgnoreCase(DBRefSource.PDB))
482           {
483             pdbRefDb = cRefDb;
484           }
485           if (cRefDb.getDbCoordSys()
486                   .equalsIgnoreCase(seqCoordSys.getName())
487                   && isAccessionMatched(cRefDb.getDbAccessionId()))
488           {
489             String resNumIndexString = cRefDb.getDbResNum()
490                     .equalsIgnoreCase("None") ? String.valueOf(UNASSIGNED)
491                     : cRefDb.getDbResNum();
492             currSeqIndex = Integer.valueOf(resNumIndexString);
493             if (pdbRefDb != null)
494             {
495               break;// exit loop if pdb and uniprot are already found
496             }
497           }
498         }
499         if (currSeqIndex == UNASSIGNED)
500         {
501           continue;
502         }
503         if (currSeqIndex > seq.getStart() && currSeqIndex <= seq.getEnd())
504         {
505           int resNum;
506           try
507           {
508             resNum = (pdbRefDb == null) ? Integer.valueOf(residue
509                   .getDbResNum()) : Integer.valueOf(pdbRefDb.getDbResNum());
510           } catch (NumberFormatException nfe)
511           {
512             resNum = (pdbRefDb == null) ? Integer.valueOf(residue
513                     .getDbResNum()) : Integer.valueOf(pdbRefDb
514                     .getDbResNum().split("[a-zA-Z]")[0]);
515           }
516
517           if (isResidueObserved(residue)
518                   || seqCoordSys == CoordinateSys.UNIPROT)
519           {
520             char resCharCode = ResidueProperties
521                     .getSingleCharacterCode(residue.getDbResName());
522             resNumMap.put(currSeqIndex, String.valueOf(resCharCode));
523           }
524           else
525           {
526             omitNonObserved.add(currSeqIndex);
527             ++nonObservedShiftIndex;
528           }
529           mapping.put(currSeqIndex - nonObservedShiftIndex, new int[] {
530               Integer.valueOf(resNum), UNASSIGNED });
531         }
532       }
533     }
534     try
535     {
536       populateAtomPositions(entityId, mapping);
537     } catch (Exception e)
538     {
539       e.printStackTrace();
540     }
541     padWithGaps(resNumMap, omitNonObserved);
542     int seqStart = UNASSIGNED;
543     int seqEnd = UNASSIGNED;
544     int pdbStart = UNASSIGNED;
545     int pdbEnd = UNASSIGNED;
546
547     Integer[] keys = mapping.keySet().toArray(new Integer[0]);
548     Arrays.sort(keys);
549     seqStart = keys[0];
550     seqEnd = keys[keys.length - 1];
551
552     String matchedSeq = originalSeq;
553     if (seqStart != UNASSIGNED)
554     {
555       pdbStart = mapping.get(seqStart)[PDB_RES_POS];
556       pdbEnd = mapping.get(seqEnd)[PDB_RES_POS];
557       int orignalSeqStart = seq.getStart();
558       if (orignalSeqStart >= 1)
559       {
560         int subSeqStart = seqStart - orignalSeqStart;
561         int subSeqEnd = seqEnd - (orignalSeqStart - 1);
562         subSeqEnd = originalSeq.length() < subSeqEnd ? originalSeq.length()
563                 : subSeqEnd;
564         matchedSeq = originalSeq.substring(subSeqStart, subSeqEnd);
565       }
566     }
567
568     StringBuilder targetStrucSeqs = new StringBuilder();
569     for (String res : resNumMap.values())
570     {
571       targetStrucSeqs.append(res);
572     }
573
574     if (os != null)
575     {
576       MappingOutputPojo mop = new MappingOutputPojo();
577       mop.setSeqStart(seqStart);
578       mop.setSeqEnd(seqEnd);
579       mop.setSeqName(seq.getName());
580       mop.setSeqResidue(matchedSeq);
581
582       mop.setStrStart(pdbStart);
583       mop.setStrEnd(pdbEnd);
584       mop.setStrName(structId);
585       mop.setStrResidue(targetStrucSeqs.toString());
586
587       mop.setType("pep");
588       os.print(getMappingOutput(mop).toString());
589     }
590     return mapping;
591   }
592
593   /**
594    * Checks if the residue instance is marked 'Not_observed' or not
595    * 
596    * @param residue
597    * @return
598    */
599   private boolean isResidueObserved(Residue residue)
600   {
601     String annotation = getResidueAnnotaiton(residue,
602             ResidueDetailType.ANNOTATION);
603     if (annotation == null)
604     {
605       return true;
606     }
607     if (!annotation.equalsIgnoreCase(NOT_FOUND)
608             && annotation.equalsIgnoreCase(NOT_OBSERVED))
609     {
610       return false;
611     }
612     return true;
613   }
614
615   /**
616    * Get annotation String for a given residue and annotation type
617    * 
618    * @param residue
619    * @param type
620    * @return
621    */
622   private String getResidueAnnotaiton(Residue residue,
623           ResidueDetailType type)
624   {
625     List<ResidueDetail> resDetails = residue.getResidueDetail();
626     for (ResidueDetail resDetail : resDetails)
627     {
628       if (resDetail.getProperty().equalsIgnoreCase(type.getCode()))
629       {
630         return resDetail.getContent();
631       }
632     }
633     return NOT_FOUND;
634   }
635
636   @Override
637   public boolean isAccessionMatched(String accession)
638   {
639     boolean isStrictMatch = true;
640     return isStrictMatch ? curSourceDBRef.equalsIgnoreCase(accession)
641             : curDBRefAccessionIdsString.contains(accession.toLowerCase());
642   }
643
644   private boolean isFoundInSiftsEntry(String accessionId)
645   {
646     return accessionId != null
647             && getAllMappingAccession().contains(accessionId);
648   }
649
650   /**
651    * Pad omitted residue positions in PDB sequence with gaps
652    * 
653    * @param resNumMap
654    */
655   void padWithGaps(TreeMap<Integer, String> resNumMap,
656           ArrayList<Integer> omitNonObserved)
657   {
658     if (resNumMap == null || resNumMap.isEmpty())
659     {
660       return;
661     }
662     Integer[] keys = resNumMap.keySet().toArray(new Integer[0]);
663     Arrays.sort(keys);
664     int firstIndex = keys[0];
665     int lastIndex = keys[keys.length - 1];
666     System.out.println("Min value " + firstIndex);
667     System.out.println("Max value " + lastIndex);
668     for (int x = firstIndex; x <= lastIndex; x++)
669     {
670       if (!resNumMap.containsKey(x) && !omitNonObserved.contains(x))
671       {
672         resNumMap.put(x, "-");
673       }
674     }
675   }
676
677
678   /**
679    * 
680    * @param chainId
681    *          Target chain to populate mapping of its atom positions.
682    * @param mapping
683    *          Two dimension array of residue index versus atom position
684    * @throws IllegalArgumentException
685    *           Thrown if chainId or mapping is null
686    */
687   void populateAtomPositions(String chainId, HashMap<Integer, int[]> mapping)
688           throws IllegalArgumentException
689   {
690     PDBChain chain = pdb.findChain(chainId);
691     if (chain == null || mapping == null)
692     {
693       throw new IllegalArgumentException(
694               "Chain id or mapping must not be null.");
695     }
696     for (int[] map : mapping.values())
697     {
698       if (map[PDB_RES_POS] != UNASSIGNED)
699       {
700         map[PDB_ATOM_POS] = getAtomIndex(map[PDB_RES_POS], chain.atoms);
701       }
702     }
703   }
704
705   /**
706    * 
707    * @param residueIndex
708    *          The residue index used for the search
709    * @param atoms
710    *          A collection of Atom to search
711    * @return atom position for the given residue index
712    */
713   int getAtomIndex(int residueIndex, Collection<Atom> atoms)
714   {
715     if (atoms == null)
716     {
717       throw new IllegalArgumentException(
718               "atoms collection must not be null!");
719     }
720     for (Atom atom : atoms)
721     {
722       if (atom.resNumber == residueIndex)
723       {
724         return atom.atomIndex;
725       }
726     }
727     return UNASSIGNED;
728   }
729
730   @Override
731   public Entity getEntityById(String id) throws SiftsException
732   {
733     List<Entity> entities = siftsEntry.getEntity();
734     for (Entity entity : entities)
735     {
736       if (!entity.getEntityId().equalsIgnoreCase(id))
737       {
738         continue;
739       }
740       return entity;
741     }
742     throw new SiftsException("Entity " + id + " not found");
743   }
744
745   @Override
746   public String[] getEntryDBs()
747   {
748     System.out.println("\nListing DB entries...");
749     List<String> availDbs = new ArrayList<String>();
750     List<Db> dbs = siftsEntry.getListDB().getDb();
751     for (Db db : dbs)
752     {
753       availDbs.add(db.getDbSource());
754       System.out.println(db.getDbSource() + " | " + db.getDbCoordSys());
755     }
756     return availDbs.toArray(new String[0]);
757   }
758
759   @Override
760   public StringBuffer getMappingOutput(MappingOutputPojo mp)
761           throws SiftsException
762   {
763     String seqRes = mp.getSeqResidue();
764     String seqName = mp.getSeqName();
765     int sStart = mp.getSeqStart();
766     int sEnd = mp.getSeqEnd();
767
768     String strRes = mp.getStrResidue();
769     String strName = mp.getStrName();
770     int pdbStart = mp.getStrStart();
771     int pdbEnd = mp.getStrEnd();
772     
773     String type = mp.getType();
774     
775     int maxid = (seqName.length() >= strName.length()) ? seqName.length()
776             : strName.length();
777     int len = 72 - maxid - 1;
778
779     int nochunks = ((seqRes.length()) / len)
780             + ((seqRes.length()) % len > 0 ? 1 : 0);
781     // output mappings
782     StringBuffer output = new StringBuffer();
783     output.append(NEWLINE);
784     output.append("Sequence ⟷ Structure mapping details").append(NEWLINE);
785     output.append("Method: SIFTS");
786     output.append(NEWLINE).append(NEWLINE);
787
788     output.append(new Format("%" + maxid + "s").form(seqName));
789     output.append(" :  ");
790     output.append(String.valueOf(sStart));
791     output.append(" - ");
792     output.append(String.valueOf(sEnd));
793     output.append(" Maps to ");
794     output.append(NEWLINE);
795     output.append(new Format("%" + maxid + "s").form(structId));
796     output.append(" :  ");
797     output.append(String.valueOf(pdbStart));
798     output.append(" - ");
799     output.append(String.valueOf(pdbEnd));
800     output.append(NEWLINE).append(NEWLINE);
801     
802     int matchedSeqCount = 0;
803     for (int j = 0; j < nochunks; j++)
804     {
805       // Print the first aligned sequence
806       output.append(new Format("%" + (maxid) + "s").form(seqName)).append(
807               " ");
808
809       for (int i = 0; i < len; i++)
810       {
811         if ((i + (j * len)) < seqRes.length())
812         {
813           output.append(seqRes.charAt(i + (j * len)));
814         }
815       }
816
817       output.append(NEWLINE);
818       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
819
820       // Print out the matching chars
821       for (int i = 0; i < len; i++)
822       {
823         try
824         {
825         if ((i + (j * len)) < seqRes.length())
826         {
827           if (seqRes.charAt(i + (j * len)) == strRes.charAt(i + (j * len))
828                   && !jalview.util.Comparison.isGap(seqRes.charAt(i
829                           + (j * len))))
830           {
831               matchedSeqCount++;
832             output.append("|");
833           }
834           else if (type.equals("pep"))
835           {
836             if (ResidueProperties.getPAM250(seqRes.charAt(i + (j * len)),
837                     strRes.charAt(i + (j * len))) > 0)
838             {
839               output.append(".");
840             }
841             else
842             {
843               output.append(" ");
844             }
845           }
846           else
847           {
848             output.append(" ");
849           }
850         }
851         } catch (IndexOutOfBoundsException e)
852         {
853           continue;
854         }
855       }
856       // Now print the second aligned sequence
857       output = output.append(NEWLINE);
858       output = output.append(new Format("%" + (maxid) + "s").form(strName))
859               .append(" ");
860       for (int i = 0; i < len; i++)
861       {
862         if ((i + (j * len)) < strRes.length())
863         {
864           output.append(strRes.charAt(i + (j * len)));
865         }
866       }
867       output.append(NEWLINE).append(NEWLINE);
868     }
869     float pid = (float) matchedSeqCount / seqRes.length() * 100;
870     if (pid < 2)
871     {
872       throw new SiftsException("Low PID detected for SIFTs mapping...");
873     }
874     output.append("Length of alignment = " + seqRes.length())
875             .append(NEWLINE);
876     output.append(new Format("Percentage ID = %2.2f").form(pid));
877     output.append(NEWLINE);
878     return output;
879   }
880   
881   @Override
882   public int getEntityCount()
883   {
884     return siftsEntry.getEntity().size();
885   }
886
887   @Override
888   public String getDbAccessionId()
889   {
890     return siftsEntry.getDbAccessionId();
891   }
892
893   @Override
894   public String getDbCoordSys()
895   {
896     return siftsEntry.getDbCoordSys();
897   }
898
899   @Override
900   public String getDbEvidence()
901   {
902     return siftsEntry.getDbEvidence();
903   }
904
905   @Override
906   public String getDbSource()
907   {
908     return siftsEntry.getDbSource();
909   }
910
911   @Override
912   public String getDbVersion()
913   {
914     return siftsEntry.getDbVersion();
915   }
916 }