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