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