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