5825a264787a48b46ccc135b40200febcb7c1e00
[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.io.StructureFile;
30 import jalview.schemes.ResidueProperties;
31 import jalview.structure.StructureMapping;
32 import jalview.structure.StructureMappingClient;
33 import jalview.structures.models.MappingOutputModel;
34 import jalview.util.Comparison;
35 import jalview.util.DBRefUtils;
36 import jalview.util.Format;
37 import jalview.xml.binding.sifts.Entry;
38 import jalview.xml.binding.sifts.Entry.Entity;
39 import jalview.xml.binding.sifts.Entry.Entity.Segment;
40 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListMapRegion.MapRegion;
41 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue;
42 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.CrossRefDb;
43 import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.ResidueDetail;
44
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.io.PrintStream;
51 import java.net.URL;
52 import java.net.URLConnection;
53 import java.nio.file.Files;
54 import java.nio.file.Path;
55 import java.nio.file.attribute.BasicFileAttributes;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collections;
59 import java.util.Date;
60 import java.util.HashMap;
61 import java.util.HashSet;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Set;
65 import java.util.TreeMap;
66 import java.util.zip.GZIPInputStream;
67
68 import javax.xml.bind.JAXBContext;
69 import javax.xml.bind.Unmarshaller;
70 import javax.xml.stream.XMLInputFactory;
71 import javax.xml.stream.XMLStreamReader;
72
73 public class SiftsClient extends StructureMappingClient implements
74         SiftsClientI
75 {
76   /*
77    * for use in mocking out file fetch for tests only
78    * - reset to null after testing!
79    */
80   private static File mockSiftsFile;
81
82   private Entry siftsEntry;
83
84   private String pdbId;
85
86   private String structId;
87
88   private CoordinateSys seqCoordSys = CoordinateSys.UNIPROT;
89
90   private static final int BUFFER_SIZE = 4096;
91
92   public static final int UNASSIGNED = -1;
93
94   private static final int PDB_RES_POS = 0;
95
96   private static final String NOT_OBSERVED = "Not_Observed";
97
98   private static final String SIFTS_FTP_BASE_URL = "http://ftp.ebi.ac.uk/pub/databases/msd/sifts/xml/";
99
100   private final static String NEWLINE = System.lineSeparator();
101
102   private String curSourceDBRef;
103
104   private HashSet<String> curDBRefAccessionIdsString;
105
106   private enum CoordinateSys
107   {
108     UNIPROT("UniProt"), PDB("PDBresnum"), PDBe("PDBe");
109     private String name;
110
111     private CoordinateSys(String name)
112     {
113       this.name = name;
114     }
115
116     public String getName()
117     {
118       return name;
119     }
120   };
121
122   private enum ResidueDetailType
123   {
124     NAME_SEC_STRUCTURE("nameSecondaryStructure"), CODE_SEC_STRUCTURE(
125             "codeSecondaryStructure"), ANNOTATION("Annotation");
126     private String code;
127
128     private ResidueDetailType(String code)
129     {
130       this.code = code;
131     }
132
133     public String getCode()
134     {
135       return code;
136     }
137   };
138
139   /**
140    * Fetch SIFTs file for the given PDBfile and construct an instance of
141    * SiftsClient
142    * 
143    * @param pdbId
144    * @throws SiftsException
145    */
146   public SiftsClient(StructureFile structureFile) throws SiftsException
147   {
148     this.structureFile = structureFile;
149     this.pdbId = structureFile.getId();
150     File siftsFile = getSiftsFile(pdbId);
151     siftsEntry = parseSIFTs(siftsFile);
152   }
153
154   /**
155    * Parse the given SIFTs File and return a JAXB POJO of parsed data
156    * 
157    * @param siftFile
158    *          - the GZipped SIFTs XML file to parse
159    * @return
160    * @throws Exception
161    *           if a problem occurs while parsing the SIFTs XML
162    */
163   private Entry parseSIFTs(File siftFile) throws SiftsException
164   {
165     try (InputStream in = new FileInputStream(siftFile);
166             GZIPInputStream gzis = new GZIPInputStream(in);)
167     {
168       // System.out.println("File : " + siftFile.getAbsolutePath());
169       JAXBContext jc = JAXBContext.newInstance("jalview.xml.binding.sifts");
170       XMLStreamReader streamReader = XMLInputFactory.newInstance()
171               .createXMLStreamReader(gzis);
172       Unmarshaller um = jc.createUnmarshaller();
173       return (Entry) um.unmarshal(streamReader);
174     } catch (Exception e)
175     {
176       e.printStackTrace();
177       throw new SiftsException(e.getMessage());
178     }
179   }
180
181   /**
182    * Get a SIFTs XML file for a given PDB Id from Cache or download from FTP
183    * repository if not found in cache
184    * 
185    * @param pdbId
186    * @return SIFTs XML file
187    * @throws SiftsException
188    */
189   public static File getSiftsFile(String pdbId) throws SiftsException
190   {
191     /*
192      * return mocked file if it has been set
193      */
194     if (mockSiftsFile != null)
195     {
196       return mockSiftsFile;
197     }
198
199     String siftsFileName = SiftsSettings.getSiftDownloadDirectory()
200             + pdbId.toLowerCase() + ".xml.gz";
201     File siftsFile = new File(siftsFileName);
202     if (siftsFile.exists())
203     {
204       // The line below is required for unit testing... don't comment it out!!!
205       System.out.println(">>> SIFTS File already downloaded for " + pdbId);
206
207       if (isFileOlderThanThreshold(siftsFile,
208               SiftsSettings.getCacheThresholdInDays()))
209       {
210         File oldSiftsFile = new File(siftsFileName + "_old");
211         siftsFile.renameTo(oldSiftsFile);
212         try
213         {
214           siftsFile = downloadSiftsFile(pdbId.toLowerCase());
215           oldSiftsFile.delete();
216           return siftsFile;
217         } catch (IOException e)
218         {
219           e.printStackTrace();
220           oldSiftsFile.renameTo(siftsFile);
221           return new File(siftsFileName);
222         }
223       }
224       else
225       {
226         return siftsFile;
227       }
228     }
229     try
230     {
231       siftsFile = downloadSiftsFile(pdbId.toLowerCase());
232     } catch (IOException e)
233     {
234       throw new SiftsException(e.getMessage());
235     }
236     return siftsFile;
237   }
238
239   /**
240    * This method enables checking if a cached file has exceeded a certain
241    * threshold(in days)
242    * 
243    * @param file
244    *          the cached file
245    * @param noOfDays
246    *          the threshold in days
247    * @return
248    */
249   public static boolean isFileOlderThanThreshold(File file, int noOfDays)
250   {
251     Path filePath = file.toPath();
252     BasicFileAttributes attr;
253     int diffInDays = 0;
254     try
255     {
256       attr = Files.readAttributes(filePath, BasicFileAttributes.class);
257       diffInDays = (int) ((new Date().getTime() - attr.lastModifiedTime()
258               .toMillis()) / (1000 * 60 * 60 * 24));
259       // System.out.println("Diff in days : " + diffInDays);
260     } catch (IOException e)
261     {
262       e.printStackTrace();
263     }
264     return noOfDays <= diffInDays;
265   }
266
267   /**
268    * Download a SIFTs XML file for a given PDB Id from an FTP repository
269    * 
270    * @param pdbId
271    * @return downloaded SIFTs XML file
272    * @throws SiftsException
273    * @throws IOException
274    */
275   public static File downloadSiftsFile(String pdbId) throws SiftsException,
276           IOException
277   {
278     if (pdbId.contains(".cif"))
279     {
280       pdbId = pdbId.replace(".cif", "");
281     }
282     String siftFile = pdbId + ".xml.gz";
283     String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
284     String downloadedSiftsFile = SiftsSettings.getSiftDownloadDirectory()
285             + siftFile;
286     File siftsDownloadDir = new File(
287             SiftsSettings.getSiftDownloadDirectory());
288     if (!siftsDownloadDir.exists())
289     {
290       siftsDownloadDir.mkdirs();
291     }
292     // System.out.println(">> Download ftp url : " + siftsFileFTPURL);
293     // long now = System.currentTimeMillis();
294     URL url = new URL(siftsFileFTPURL);
295     URLConnection conn = url.openConnection();
296     InputStream inputStream = conn.getInputStream();
297     FileOutputStream outputStream = new FileOutputStream(
298             downloadedSiftsFile);
299     byte[] buffer = new byte[BUFFER_SIZE];
300     int bytesRead = -1;
301     while ((bytesRead = inputStream.read(buffer)) != -1)
302     {
303       outputStream.write(buffer, 0, bytesRead);
304     }
305     outputStream.close();
306     inputStream.close();
307 //    System.out.println(">>> File downloaded : " + downloadedSiftsFile
308 //            + " took " + (System.currentTimeMillis() - now) + "ms");
309     return new File(downloadedSiftsFile);
310   }
311
312   /**
313    * Delete the SIFTs file for the given PDB Id in the local SIFTs download
314    * directory
315    * 
316    * @param pdbId
317    * @return true if the file was deleted or doesn't exist
318    */
319   public static boolean deleteSiftsFileByPDBId(String pdbId)
320   {
321     File siftsFile = new File(SiftsSettings.getSiftDownloadDirectory()
322             + pdbId.toLowerCase() + ".xml.gz");
323     if (siftsFile.exists())
324     {
325       return siftsFile.delete();
326     }
327     return true;
328   }
329
330   /**
331    * Get a valid SIFTs DBRef for the given sequence current SIFTs entry
332    * 
333    * @param seq
334    *          - the target sequence for the operation
335    * @return a valid DBRefEntry that is SIFTs compatible
336    * @throws Exception
337    *           if no valid source DBRefEntry was found for the given sequences
338    */
339   public DBRefEntryI getValidSourceDBRef(SequenceI seq)
340           throws SiftsException
341   {
342     List<DBRefEntry> dbRefs = seq.getPrimaryDBRefs();
343     if (dbRefs == null || dbRefs.size() < 1)
344     {
345       throw new SiftsException(
346               "Source DBRef could not be determined. DBRefs might not have been retrieved.");
347     }
348
349     for (DBRefEntry dbRef : dbRefs)
350     {
351       if (dbRef == null || dbRef.getAccessionId() == null
352               || dbRef.getSource() == null)
353       {
354         continue;
355       }
356       String canonicalSource = DBRefUtils.getCanonicalName(dbRef
357               .getSource());
358       if (isValidDBRefEntry(dbRef)
359               && (canonicalSource.equalsIgnoreCase(DBRefSource.UNIPROT) || canonicalSource
360                       .equalsIgnoreCase(DBRefSource.PDB)))
361       {
362         return dbRef;
363       }
364     }
365     throw new SiftsException("Could not get source DB Ref");
366   }
367
368   /**
369    * Check that the DBRef Entry is properly populated and is available in this
370    * SiftClient instance
371    * 
372    * @param entry
373    *          - DBRefEntry to validate
374    * @return true validation is successful otherwise false is returned.
375    */
376   boolean isValidDBRefEntry(DBRefEntryI entry)
377   {
378     return entry != null && entry.getAccessionId() != null
379             && isFoundInSiftsEntry(entry.getAccessionId());
380   }
381
382   @Override
383   public HashSet<String> getAllMappingAccession()
384   {
385     HashSet<String> accessions = new HashSet<String>();
386     List<Entity> entities = siftsEntry.getEntity();
387     for (Entity entity : entities)
388     {
389       List<Segment> segments = entity.getSegment();
390       for (Segment segment : segments)
391       {
392         List<MapRegion> mapRegions = segment.getListMapRegion()
393                 .getMapRegion();
394         for (MapRegion mapRegion : mapRegions)
395         {
396           accessions
397                   .add(mapRegion.getDb().getDbAccessionId().toLowerCase());
398         }
399       }
400     }
401     return accessions;
402   }
403
404   @Override
405   public StructureMapping getStructureMapping(SequenceI seq,
406           String pdbFile, String chain) throws Exception,
407           StructureMappingException
408   {
409     structId = (chain == null) ? pdbId : pdbId + "|" + chain;
410     System.out.println("Getting SIFTS mapping for " + structId + ": seq "
411             + seq.getName());
412
413     final StringBuilder mappingDetails = new StringBuilder(128);
414     PrintStream ps = new PrintStream(System.out)
415     {
416       @Override
417       public void print(String x)
418       {
419         mappingDetails.append(x);
420       }
421
422       @Override
423       public void println()
424       {
425         mappingDetails.append(NEWLINE);
426       }
427     };
428     HashMap<Integer, int[]> mapping = getGreedyMapping(chain, seq, ps);
429
430     String mappingOutput = mappingDetails.toString();
431     StructureMapping siftsMapping = new StructureMapping(seq, pdbFile,
432             pdbId, chain, mapping, mappingOutput);
433     return siftsMapping;
434   }
435
436   @Override
437   public HashMap<Integer, int[]> getGreedyMapping(String entityId,
438           SequenceI seq, java.io.PrintStream os) throws SiftsException,
439           StructureMappingException
440   {
441     List<Integer> omitNonObserved = new ArrayList<Integer>();
442     int nonObservedShiftIndex = 0;
443     // System.out.println("Generating mappings for : " + entityId);
444     Entity entity = null;
445     entity = getEntityById(entityId);
446     String originalSeq = AlignSeq.extractGaps(
447             jalview.util.Comparison.GapChars, seq.getSequenceAsString());
448     HashMap<Integer, int[]> mapping = new HashMap<Integer, int[]>();
449     DBRefEntryI sourceDBRef;
450     sourceDBRef = getValidSourceDBRef(seq);
451     // TODO ensure sequence start/end is in the same coordinate system and
452     // consistent with the choosen sourceDBRef
453
454     // set sequence coordinate system - default value is UniProt
455     if (sourceDBRef.getSource().equalsIgnoreCase(DBRefSource.PDB))
456     {
457       seqCoordSys = CoordinateSys.PDB;
458     }
459
460     HashSet<String> dbRefAccessionIdsString = new HashSet<String>();
461     for (DBRefEntry dbref : seq.getDBRefs())
462     {
463       dbRefAccessionIdsString.add(dbref.getAccessionId().toLowerCase());
464     }
465     dbRefAccessionIdsString.add(sourceDBRef.getAccessionId().toLowerCase());
466
467     curDBRefAccessionIdsString = dbRefAccessionIdsString;
468     curSourceDBRef = sourceDBRef.getAccessionId();
469
470     TreeMap<Integer, String> resNumMap = new TreeMap<Integer, String>();
471     List<Segment> segments = entity.getSegment();
472     SegmentHelperPojo shp = new SegmentHelperPojo(seq, mapping, resNumMap,
473             omitNonObserved, nonObservedShiftIndex);
474     processSegments(segments, shp);
475     try
476     {
477       populateAtomPositions(entityId, mapping);
478     } catch (Exception e)
479     {
480       e.printStackTrace();
481     }
482     if (seqCoordSys == CoordinateSys.UNIPROT)
483     {
484       padWithGaps(resNumMap, omitNonObserved);
485     }
486     int seqStart = UNASSIGNED;
487     int seqEnd = UNASSIGNED;
488     int pdbStart = UNASSIGNED;
489     int pdbEnd = UNASSIGNED;
490
491     if (mapping.isEmpty())
492     {
493       throw new SiftsException("SIFTS mapping failed");
494     }
495
496     Integer[] keys = mapping.keySet().toArray(new Integer[0]);
497     Arrays.sort(keys);
498     seqStart = keys[0];
499     seqEnd = keys[keys.length - 1];
500
501     String matchedSeq = originalSeq;
502     if (seqStart != UNASSIGNED)
503     {
504       pdbStart = mapping.get(seqStart)[PDB_RES_POS];
505       pdbEnd = mapping.get(seqEnd)[PDB_RES_POS];
506       int orignalSeqStart = seq.getStart();
507       if (orignalSeqStart >= 1)
508       {
509         int subSeqStart = (seqStart >= orignalSeqStart) ? seqStart
510                 - orignalSeqStart : 0;
511         int subSeqEnd = seqEnd - (orignalSeqStart - 1);
512         subSeqEnd = originalSeq.length() < subSeqEnd ? originalSeq.length()
513                 : subSeqEnd;
514         matchedSeq = originalSeq.substring(subSeqStart, subSeqEnd);
515       }
516       else
517       {
518         matchedSeq = originalSeq.substring(1, originalSeq.length());
519       }
520     }
521
522     StringBuilder targetStrucSeqs = new StringBuilder();
523     for (String res : resNumMap.values())
524     {
525       targetStrucSeqs.append(res);
526     }
527
528     if (os != null)
529     {
530       MappingOutputModel mop = new MappingOutputModel();
531       mop.setSeqStart(seqStart);
532       mop.setSeqEnd(seqEnd);
533       mop.setSeqName(seq.getName());
534       mop.setSeqResidue(matchedSeq);
535
536       mop.setStrStart(pdbStart);
537       mop.setStrEnd(pdbEnd);
538       mop.setStrName(structId);
539       mop.setStrResidue(targetStrucSeqs.toString());
540
541       mop.setType("pep");
542       os.print(getMappingOutput(mop).toString());
543       os.println();
544     }
545     return mapping;
546   }
547
548   void processSegments(List<Segment> segments, SegmentHelperPojo shp)
549   {
550     SequenceI seq = shp.getSeq();
551     HashMap<Integer, int[]> mapping = shp.getMapping();
552     TreeMap<Integer, String> resNumMap = shp.getResNumMap();
553     List<Integer> omitNonObserved = shp.getOmitNonObserved();
554     int nonObservedShiftIndex = shp.getNonObservedShiftIndex();
555     for (Segment segment : segments)
556     {
557       // System.out.println("Mapping segments : " + segment.getSegId() + "\\"s
558       // + segStartEnd);
559       List<Residue> residues = segment.getListResidue().getResidue();
560       for (Residue residue : residues)
561       {
562         int currSeqIndex = UNASSIGNED;
563         List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
564         CrossRefDb pdbRefDb = null;
565         for (CrossRefDb cRefDb : cRefDbs)
566         {
567           if (cRefDb.getDbSource().equalsIgnoreCase(DBRefSource.PDB))
568           {
569             pdbRefDb = cRefDb;
570           }
571           if (cRefDb.getDbCoordSys()
572                   .equalsIgnoreCase(seqCoordSys.getName())
573                   && isAccessionMatched(cRefDb.getDbAccessionId()))
574           {
575             String resNumIndexString = cRefDb.getDbResNum()
576                     .equalsIgnoreCase("None") ? String.valueOf(UNASSIGNED)
577                     : cRefDb.getDbResNum();
578             try
579             {
580               currSeqIndex = Integer.valueOf(resNumIndexString);
581             } catch (NumberFormatException nfe)
582             {
583               currSeqIndex = Integer.valueOf(resNumIndexString
584                       .split("[a-zA-Z]")[0]);
585               continue;
586             }
587             if (pdbRefDb != null)
588             {
589               break;// exit loop if pdb and uniprot are already found
590             }
591           }
592         }
593         if (currSeqIndex == UNASSIGNED)
594         {
595           continue;
596         }
597         if (currSeqIndex >= seq.getStart() && currSeqIndex <= seq.getEnd())
598         {
599           int resNum;
600           try
601           {
602             resNum = (pdbRefDb == null) ? Integer.valueOf(residue
603                     .getDbResNum()) : Integer.valueOf(pdbRefDb
604                     .getDbResNum());
605           } catch (NumberFormatException nfe)
606           {
607             if (pdbRefDb == null || pdbRefDb.getDbResNum().equals("null"))
608             {
609               resNum = UNASSIGNED;
610               continue;
611             }
612             resNum = Integer.valueOf(pdbRefDb
613                     .getDbResNum().split("[a-zA-Z]")[0]);
614             continue;
615           }
616
617           if (isResidueObserved(residue)
618                   || seqCoordSys == CoordinateSys.UNIPROT)
619           {
620             char resCharCode = ResidueProperties
621                     .getSingleCharacterCode(ResidueProperties
622                             .getCanonicalAminoAcid(residue.getDbResName()));
623             resNumMap.put(currSeqIndex, String.valueOf(resCharCode));
624           }
625           else
626           {
627             omitNonObserved.add(currSeqIndex);
628             ++nonObservedShiftIndex;
629           }
630           mapping.put(currSeqIndex - nonObservedShiftIndex, new int[] {
631               Integer.valueOf(resNum), UNASSIGNED });
632         }
633       }
634     }
635   }
636
637
638   /**
639    * Checks if the residue instance is marked 'Not_observed' or not
640    * 
641    * @param residue
642    * @return
643    */
644   private boolean isResidueObserved(Residue residue)
645   {
646     Set<String> annotations = getResidueAnnotaitons(residue,
647             ResidueDetailType.ANNOTATION);
648     if (annotations == null || annotations.isEmpty())
649     {
650       return true;
651     }
652     for (String annotation : annotations)
653     {
654       if (annotation.equalsIgnoreCase(NOT_OBSERVED))
655       {
656         return false;
657       }
658     }
659     return true;
660   }
661
662   /**
663    * Get annotation String for a given residue and annotation type
664    * 
665    * @param residue
666    * @param type
667    * @return
668    */
669   private Set<String> getResidueAnnotaitons(Residue residue,
670           ResidueDetailType type)
671   {
672     HashSet<String> foundAnnotations = new HashSet<String>();
673     List<ResidueDetail> resDetails = residue.getResidueDetail();
674     for (ResidueDetail resDetail : resDetails)
675     {
676       if (resDetail.getProperty().equalsIgnoreCase(type.getCode()))
677       {
678         foundAnnotations.add(resDetail.getContent());
679       }
680     }
681     return foundAnnotations;
682   }
683
684   @Override
685   public boolean isAccessionMatched(String accession)
686   {
687     boolean isStrictMatch = true;
688     return isStrictMatch ? curSourceDBRef.equalsIgnoreCase(accession)
689             : curDBRefAccessionIdsString.contains(accession.toLowerCase());
690   }
691
692   private boolean isFoundInSiftsEntry(String accessionId)
693   {
694     Set<String> siftsDBRefs = getAllMappingAccession();
695     return accessionId != null
696             && siftsDBRefs.contains(accessionId.toLowerCase());
697   }
698
699   /**
700    * Pad omitted residue positions in PDB sequence with gaps
701    * 
702    * @param resNumMap
703    */
704   void padWithGaps(Map<Integer, String> resNumMap,
705           List<Integer> omitNonObserved)
706   {
707     if (resNumMap == null || resNumMap.isEmpty())
708     {
709       return;
710     }
711     Integer[] keys = resNumMap.keySet().toArray(new Integer[0]);
712     // Arrays.sort(keys);
713     int firstIndex = keys[0];
714     int lastIndex = keys[keys.length - 1];
715     // System.out.println("Min value " + firstIndex);
716     // System.out.println("Max value " + lastIndex);
717     for (int x = firstIndex; x <= lastIndex; x++)
718     {
719       if (!resNumMap.containsKey(x) && !omitNonObserved.contains(x))
720       {
721         resNumMap.put(x, "-");
722       }
723     }
724   }
725
726   @Override
727   public Entity getEntityById(String id) throws SiftsException
728   {
729     // Determines an entity to process by performing a heuristic matching of all
730     // Entities with the given chainId and choosing the best matching Entity
731     Entity entity = getEntityByMostOptimalMatchedId(id);
732     if (entity != null)
733     {
734       return entity;
735     }
736     throw new SiftsException("Entity " + id + " not found");
737   }
738
739   /**
740    * This method was added because EntityId is NOT always equal to ChainId.
741    * Hence, it provides the logic to greedily detect the "true" Entity for a
742    * given chainId where discrepancies exist.
743    * 
744    * @param chainId
745    * @return
746    */
747   public Entity getEntityByMostOptimalMatchedId(String chainId)
748   {
749     // System.out.println("---> advanced greedy entityId matching block entered..");
750     List<Entity> entities = siftsEntry.getEntity();
751     SiftsEntitySortPojo[] sPojo = new SiftsEntitySortPojo[entities.size()];
752     int count = 0;
753     for (Entity entity : entities)
754     {
755       sPojo[count] = new SiftsEntitySortPojo();
756       sPojo[count].entityId = entity.getEntityId();
757
758       List<Segment> segments = entity.getSegment();
759       for (Segment segment : segments)
760       {
761         List<Residue> residues = segment.getListResidue().getResidue();
762         for (Residue residue : residues)
763         {
764           List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
765           for (CrossRefDb cRefDb : cRefDbs)
766           {
767             if (!cRefDb.getDbSource().equalsIgnoreCase("PDB"))
768             {
769               continue;
770             }
771             ++sPojo[count].resCount;
772             if (cRefDb.getDbChainId().equalsIgnoreCase(chainId))
773             {
774               ++sPojo[count].chainIdFreq;
775             }
776           }
777         }
778       }
779       sPojo[count].pid = (100 * sPojo[count].chainIdFreq)
780               / sPojo[count].resCount;
781       ++count;
782     }
783     Arrays.sort(sPojo, Collections.reverseOrder());
784     // System.out.println("highest matched entity : " + sPojo[0].entityId);
785     // System.out.println("highest matched pid : " + sPojo[0].pid);
786
787     if (sPojo[0].entityId != null)
788     {
789       if (sPojo[0].pid < 1)
790       {
791         return null;
792       }
793       for (Entity entity : entities)
794       {
795         if (!entity.getEntityId().equalsIgnoreCase(sPojo[0].entityId))
796         {
797           continue;
798         }
799         return entity;
800       }
801     }
802     return null;
803   }
804
805   private class SiftsEntitySortPojo implements
806           Comparable<SiftsEntitySortPojo>
807   {
808     public String entityId;
809
810     public int chainIdFreq;
811
812     public int pid;
813
814     public int resCount;
815
816     @Override
817     public int compareTo(SiftsEntitySortPojo o)
818     {
819       return this.pid - o.pid;
820     }
821   }
822
823   private class SegmentHelperPojo
824   {
825     private SequenceI seq;
826
827     private HashMap<Integer, int[]> mapping;
828
829     private TreeMap<Integer, String> resNumMap;
830
831     private List<Integer> omitNonObserved;
832
833     private int nonObservedShiftIndex;
834
835     public SegmentHelperPojo(SequenceI seq,
836             HashMap<Integer, int[]> mapping,
837             TreeMap<Integer, String> resNumMap,
838             List<Integer> omitNonObserved, int nonObservedShiftIndex)
839     {
840       setSeq(seq);
841       setMapping(mapping);
842       setResNumMap(resNumMap);
843       setOmitNonObserved(omitNonObserved);
844       setNonObservedShiftIndex(nonObservedShiftIndex);
845     }
846
847     public SequenceI getSeq()
848     {
849       return seq;
850     }
851
852     public void setSeq(SequenceI seq)
853     {
854       this.seq = seq;
855     }
856
857     public HashMap<Integer, int[]> getMapping()
858     {
859       return mapping;
860     }
861
862     public void setMapping(HashMap<Integer, int[]> mapping)
863     {
864       this.mapping = mapping;
865     }
866
867     public TreeMap<Integer, String> getResNumMap()
868     {
869       return resNumMap;
870     }
871
872     public void setResNumMap(TreeMap<Integer, String> resNumMap)
873     {
874       this.resNumMap = resNumMap;
875     }
876
877     public List<Integer> getOmitNonObserved()
878     {
879       return omitNonObserved;
880     }
881
882     public void setOmitNonObserved(List<Integer> omitNonObserved)
883     {
884       this.omitNonObserved = omitNonObserved;
885     }
886
887     public int getNonObservedShiftIndex()
888     {
889       return nonObservedShiftIndex;
890     }
891
892     public void setNonObservedShiftIndex(int nonObservedShiftIndex)
893     {
894       this.nonObservedShiftIndex = nonObservedShiftIndex;
895     }
896   }
897
898   @Override
899   public StringBuffer getMappingOutput(MappingOutputModel mp)
900           throws StructureMappingException
901   {
902     String seqRes = mp.getSeqResidue();
903     String seqName = mp.getSeqName();
904     int sStart = mp.getSeqStart();
905     int sEnd = mp.getSeqEnd();
906
907     String strRes = mp.getStrResidue();
908     String strName = mp.getStrName();
909     int pdbStart = mp.getStrStart();
910     int pdbEnd = mp.getStrEnd();
911
912     String type = mp.getType();
913
914     int maxid = (seqName.length() >= strName.length()) ? seqName.length()
915             : strName.length();
916     int len = 72 - maxid - 1;
917
918     int nochunks = ((seqRes.length()) / len)
919             + ((seqRes.length()) % len > 0 ? 1 : 0);
920     // output mappings
921     StringBuffer output = new StringBuffer();
922     output.append(NEWLINE);
923     output.append("Sequence \u27f7 Structure mapping details").append(
924             NEWLINE);
925     output.append("Method: SIFTS");
926     output.append(NEWLINE).append(NEWLINE);
927
928     output.append(new Format("%" + maxid + "s").form(seqName));
929     output.append(" :  ");
930     output.append(String.valueOf(sStart));
931     output.append(" - ");
932     output.append(String.valueOf(sEnd));
933     output.append(" Maps to ");
934     output.append(NEWLINE);
935     output.append(new Format("%" + maxid + "s").form(structId));
936     output.append(" :  ");
937     output.append(String.valueOf(pdbStart));
938     output.append(" - ");
939     output.append(String.valueOf(pdbEnd));
940     output.append(NEWLINE).append(NEWLINE);
941
942     int matchedSeqCount = 0;
943     for (int j = 0; j < nochunks; j++)
944     {
945       // Print the first aligned sequence
946       output.append(new Format("%" + (maxid) + "s").form(seqName)).append(
947               " ");
948
949       for (int i = 0; i < len; i++)
950       {
951         if ((i + (j * len)) < seqRes.length())
952         {
953           output.append(seqRes.charAt(i + (j * len)));
954         }
955       }
956
957       output.append(NEWLINE);
958       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
959
960       // Print out the matching chars
961       for (int i = 0; i < len; i++)
962       {
963         try
964         {
965           if ((i + (j * len)) < seqRes.length())
966           {
967             boolean sameChar = Comparison.isSameResidue(
968                     seqRes.charAt(i + (j * len)),
969                     strRes.charAt(i + (j * len)), false);
970             if (sameChar
971                     && !jalview.util.Comparison.isGap(seqRes.charAt(i
972                             + (j * len))))
973             {
974               matchedSeqCount++;
975               output.append("|");
976             }
977             else if (type.equals("pep"))
978             {
979               if (ResidueProperties.getPAM250(seqRes.charAt(i + (j * len)),
980                       strRes.charAt(i + (j * len))) > 0)
981               {
982                 output.append(".");
983               }
984               else
985               {
986                 output.append(" ");
987               }
988             }
989             else
990             {
991               output.append(" ");
992             }
993           }
994         } catch (IndexOutOfBoundsException e)
995         {
996           continue;
997         }
998       }
999       // Now print the second aligned sequence
1000       output = output.append(NEWLINE);
1001       output = output.append(new Format("%" + (maxid) + "s").form(strName))
1002               .append(" ");
1003       for (int i = 0; i < len; i++)
1004       {
1005         if ((i + (j * len)) < strRes.length())
1006         {
1007           output.append(strRes.charAt(i + (j * len)));
1008         }
1009       }
1010       output.append(NEWLINE).append(NEWLINE);
1011     }
1012     float pid = (float) matchedSeqCount / seqRes.length() * 100;
1013     if (pid < SiftsSettings.getFailSafePIDThreshold())
1014     {
1015       throw new StructureMappingException(
1016               ">>> Low PID detected for SIFTs mapping...");
1017     }
1018     output.append("Length of alignment = " + seqRes.length()).append(
1019             NEWLINE);
1020     output.append(new Format("Percentage ID = %2.2f").form(pid));
1021     return output;
1022   }
1023
1024   @Override
1025   public int getEntityCount()
1026   {
1027     return siftsEntry.getEntity().size();
1028   }
1029
1030   @Override
1031   public String getDbAccessionId()
1032   {
1033     return siftsEntry.getDbAccessionId();
1034   }
1035
1036   @Override
1037   public String getDbCoordSys()
1038   {
1039     return siftsEntry.getDbCoordSys();
1040   }
1041
1042   @Override
1043   public String getDbSource()
1044   {
1045     return siftsEntry.getDbSource();
1046   }
1047
1048   @Override
1049   public String getDbVersion()
1050   {
1051     return siftsEntry.getDbVersion();
1052   }
1053
1054   public static void setMockSiftsFile(File file)
1055   {
1056     mockSiftsFile = file;
1057   }
1058
1059 }