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