JAL-2780 JAL-2781 should always ensure SIFTS mappings are made to dataset sequence...
[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     SequenceI aseq = seq;
417     while (seq.getDatasetSequence() != null)
418     {
419       seq = seq.getDatasetSequence();
420     }
421     structId = (chain == null) ? pdbId : pdbId + "|" + chain;
422     System.out.println("Getting SIFTS mapping for " + structId + ": seq "
423             + seq.getName());
424
425     final StringBuilder mappingDetails = new StringBuilder(128);
426     PrintStream ps = new PrintStream(System.out)
427     {
428       @Override
429       public void print(String x)
430       {
431         mappingDetails.append(x);
432       }
433
434       @Override
435       public void println()
436       {
437         mappingDetails.append(NEWLINE);
438       }
439     };
440     HashMap<Integer, int[]> mapping = getGreedyMapping(chain, seq, ps);
441
442     String mappingOutput = mappingDetails.toString();
443     StructureMapping siftsMapping = new StructureMapping(seq, pdbFile,
444             pdbId, chain, mapping, mappingOutput);
445     return siftsMapping;
446   }
447
448   @Override
449   public HashMap<Integer, int[]> getGreedyMapping(String entityId,
450           SequenceI seq, java.io.PrintStream os) throws SiftsException
451   {
452     List<Integer> omitNonObserved = new ArrayList<Integer>();
453     int nonObservedShiftIndex = 0;
454     // System.out.println("Generating mappings for : " + entityId);
455     Entity entity = null;
456     entity = getEntityById(entityId);
457     String originalSeq = AlignSeq.extractGaps(
458             jalview.util.Comparison.GapChars, seq.getSequenceAsString());
459     HashMap<Integer, int[]> mapping = new HashMap<Integer, int[]>();
460     DBRefEntryI sourceDBRef;
461     sourceDBRef = getValidSourceDBRef(seq);
462     // TODO ensure sequence start/end is in the same coordinate system and
463     // consistent with the choosen sourceDBRef
464
465     // set sequence coordinate system - default value is UniProt
466     if (sourceDBRef.getSource().equalsIgnoreCase(DBRefSource.PDB))
467     {
468       seqCoordSys = CoordinateSys.PDB;
469     }
470
471     HashSet<String> dbRefAccessionIdsString = new HashSet<String>();
472     for (DBRefEntry dbref : seq.getDBRefs())
473     {
474       dbRefAccessionIdsString.add(dbref.getAccessionId().toLowerCase());
475     }
476     dbRefAccessionIdsString.add(sourceDBRef.getAccessionId().toLowerCase());
477
478     curDBRefAccessionIdsString = dbRefAccessionIdsString;
479     curSourceDBRef = sourceDBRef.getAccessionId();
480
481     TreeMap<Integer, String> resNumMap = new TreeMap<Integer, String>();
482     List<Segment> segments = entity.getSegment();
483     SegmentHelperPojo shp = new SegmentHelperPojo(seq, mapping, resNumMap,
484             omitNonObserved, nonObservedShiftIndex);
485     processSegments(segments, shp);
486     try
487     {
488       populateAtomPositions(entityId, mapping);
489     } catch (Exception e)
490     {
491       e.printStackTrace();
492     }
493     if (seqCoordSys == CoordinateSys.UNIPROT)
494     {
495       padWithGaps(resNumMap, omitNonObserved);
496     }
497     int seqStart = UNASSIGNED;
498     int seqEnd = UNASSIGNED;
499     int pdbStart = UNASSIGNED;
500     int pdbEnd = UNASSIGNED;
501
502     if (mapping.isEmpty())
503     {
504       throw new SiftsException("SIFTS mapping failed");
505     }
506
507     Integer[] keys = mapping.keySet().toArray(new Integer[0]);
508     Arrays.sort(keys);
509     seqStart = keys[0];
510     seqEnd = keys[keys.length - 1];
511
512     String matchedSeq = originalSeq;
513     if (seqStart != UNASSIGNED)
514     {
515       pdbStart = mapping.get(seqStart)[PDB_RES_POS];
516       pdbEnd = mapping.get(seqEnd)[PDB_RES_POS];
517       int orignalSeqStart = seq.getStart();
518       if (orignalSeqStart >= 1)
519       {
520         int subSeqStart = (seqStart >= orignalSeqStart)
521                 ? seqStart - orignalSeqStart
522                 : 0;
523         int subSeqEnd = seqEnd - (orignalSeqStart - 1);
524         subSeqEnd = originalSeq.length() < subSeqEnd ? originalSeq.length()
525                 : subSeqEnd;
526         matchedSeq = originalSeq.substring(subSeqStart, subSeqEnd);
527       }
528       else
529       {
530         matchedSeq = originalSeq.substring(1, originalSeq.length());
531       }
532     }
533
534     StringBuilder targetStrucSeqs = new StringBuilder();
535     for (String res : resNumMap.values())
536     {
537       targetStrucSeqs.append(res);
538     }
539
540     if (os != null)
541     {
542       MappingOutputPojo mop = new MappingOutputPojo();
543       mop.setSeqStart(seqStart);
544       mop.setSeqEnd(seqEnd);
545       mop.setSeqName(seq.getName());
546       mop.setSeqResidue(matchedSeq);
547
548       mop.setStrStart(pdbStart);
549       mop.setStrEnd(pdbEnd);
550       mop.setStrName(structId);
551       mop.setStrResidue(targetStrucSeqs.toString());
552
553       mop.setType("pep");
554       os.print(getMappingOutput(mop).toString());
555       os.println();
556     }
557     return mapping;
558   }
559
560   void processSegments(List<Segment> segments, SegmentHelperPojo shp)
561   {
562     SequenceI seq = shp.getSeq();
563     HashMap<Integer, int[]> mapping = shp.getMapping();
564     TreeMap<Integer, String> resNumMap = shp.getResNumMap();
565     List<Integer> omitNonObserved = shp.getOmitNonObserved();
566     int nonObservedShiftIndex = shp.getNonObservedShiftIndex();
567     for (Segment segment : segments)
568     {
569       // System.out.println("Mapping segments : " + segment.getSegId() + "\\"s
570       // + segStartEnd);
571       List<Residue> residues = segment.getListResidue().getResidue();
572       for (Residue residue : residues)
573       {
574         int currSeqIndex = UNASSIGNED;
575         List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
576         CrossRefDb pdbRefDb = null;
577         for (CrossRefDb cRefDb : cRefDbs)
578         {
579           if (cRefDb.getDbSource().equalsIgnoreCase(DBRefSource.PDB))
580           {
581             pdbRefDb = cRefDb;
582           }
583           if (cRefDb.getDbCoordSys().equalsIgnoreCase(seqCoordSys.getName())
584                   && isAccessionMatched(cRefDb.getDbAccessionId()))
585           {
586             currSeqIndex = getLeadingIntegerValue(cRefDb.getDbResNum(),
587                     UNASSIGNED);
588             if (pdbRefDb != null)
589             {
590               break;// exit loop if pdb and uniprot are already found
591             }
592           }
593         }
594         if (currSeqIndex == UNASSIGNED)
595         {
596           continue;
597         }
598         if (currSeqIndex >= seq.getStart() && currSeqIndex <= seq.getEnd())
599         {
600
601           int resNum = (pdbRefDb == null)
602                   ? getLeadingIntegerValue(residue.getDbResNum(),
603                           UNASSIGNED)
604                   : getLeadingIntegerValue(pdbRefDb.getDbResNum(),
605                           UNASSIGNED);
606
607           if (isResidueObserved(residue)
608                   || seqCoordSys == CoordinateSys.UNIPROT)
609           {
610             char resCharCode = ResidueProperties
611                     .getSingleCharacterCode(ResidueProperties
612                             .getCanonicalAminoAcid(residue.getDbResName()));
613             resNumMap.put(currSeqIndex, String.valueOf(resCharCode));
614           }
615           else
616           {
617             omitNonObserved.add(currSeqIndex);
618             ++nonObservedShiftIndex;
619           }
620           mapping.put(currSeqIndex - nonObservedShiftIndex,
621                   new int[]
622                   { Integer.valueOf(resNum), UNASSIGNED });
623         }
624       }
625     }
626   }
627
628   /**
629    * Get the leading integer part of a string that begins with an integer.
630    * 
631    * @param input
632    *          - the string input to process
633    * @param failValue
634    *          - value returned if unsuccessful
635    * @return
636    */
637   static int getLeadingIntegerValue(String input, int failValue)
638   {
639     if (input == null)
640     {
641       return failValue;
642     }
643     String[] parts = input.split("(?=\\D)(?<=\\d)");
644     if (parts != null && parts.length > 0 && parts[0].matches("[0-9]+"))
645     {
646       return Integer.valueOf(parts[0]);
647     }
648     return failValue;
649   }
650
651   /**
652    * 
653    * @param chainId
654    *          Target chain to populate mapping of its atom positions.
655    * @param mapping
656    *          Two dimension array of residue index versus atom position
657    * @throws IllegalArgumentException
658    *           Thrown if chainId or mapping is null
659    * @throws SiftsException
660    */
661   void populateAtomPositions(String chainId, Map<Integer, int[]> mapping)
662           throws IllegalArgumentException, SiftsException
663   {
664     try
665     {
666       PDBChain chain = pdb.findChain(chainId);
667
668       if (chain == null || mapping == null)
669       {
670         throw new IllegalArgumentException(
671                 "Chain id or mapping must not be null.");
672       }
673       for (int[] map : mapping.values())
674       {
675         if (map[PDB_RES_POS] != UNASSIGNED)
676         {
677           map[PDB_ATOM_POS] = getAtomIndex(map[PDB_RES_POS], chain.atoms);
678         }
679       }
680     } catch (NullPointerException e)
681     {
682       throw new SiftsException(e.getMessage());
683     } catch (Exception e)
684     {
685       throw new SiftsException(e.getMessage());
686     }
687   }
688
689   /**
690    * 
691    * @param residueIndex
692    *          The residue index used for the search
693    * @param atoms
694    *          A collection of Atom to search
695    * @return atom position for the given residue index
696    */
697   int getAtomIndex(int residueIndex, Collection<Atom> atoms)
698   {
699     if (atoms == null)
700     {
701       throw new IllegalArgumentException(
702               "atoms collection must not be null!");
703     }
704     for (Atom atom : atoms)
705     {
706       if (atom.resNumber == residueIndex)
707       {
708         return atom.atomIndex;
709       }
710     }
711     return UNASSIGNED;
712   }
713
714   /**
715    * Checks if the residue instance is marked 'Not_observed' or not
716    * 
717    * @param residue
718    * @return
719    */
720   private boolean isResidueObserved(Residue residue)
721   {
722     Set<String> annotations = getResidueAnnotaitons(residue,
723             ResidueDetailType.ANNOTATION);
724     if (annotations == null || annotations.isEmpty())
725     {
726       return true;
727     }
728     for (String annotation : annotations)
729     {
730       if (annotation.equalsIgnoreCase(NOT_OBSERVED))
731       {
732         return false;
733       }
734     }
735     return true;
736   }
737
738   /**
739    * Get annotation String for a given residue and annotation type
740    * 
741    * @param residue
742    * @param type
743    * @return
744    */
745   private Set<String> getResidueAnnotaitons(Residue residue,
746           ResidueDetailType type)
747   {
748     HashSet<String> foundAnnotations = new HashSet<String>();
749     List<ResidueDetail> resDetails = residue.getResidueDetail();
750     for (ResidueDetail resDetail : resDetails)
751     {
752       if (resDetail.getProperty().equalsIgnoreCase(type.getCode()))
753       {
754         foundAnnotations.add(resDetail.getContent());
755       }
756     }
757     return foundAnnotations;
758   }
759
760   @Override
761   public boolean isAccessionMatched(String accession)
762   {
763     boolean isStrictMatch = true;
764     return isStrictMatch ? curSourceDBRef.equalsIgnoreCase(accession)
765             : curDBRefAccessionIdsString.contains(accession.toLowerCase());
766   }
767
768   private boolean isFoundInSiftsEntry(String accessionId)
769   {
770     Set<String> siftsDBRefs = getAllMappingAccession();
771     return accessionId != null
772             && siftsDBRefs.contains(accessionId.toLowerCase());
773   }
774
775   /**
776    * Pad omitted residue positions in PDB sequence with gaps
777    * 
778    * @param resNumMap
779    */
780   void padWithGaps(Map<Integer, String> resNumMap,
781           List<Integer> omitNonObserved)
782   {
783     if (resNumMap == null || resNumMap.isEmpty())
784     {
785       return;
786     }
787     Integer[] keys = resNumMap.keySet().toArray(new Integer[0]);
788     // Arrays.sort(keys);
789     int firstIndex = keys[0];
790     int lastIndex = keys[keys.length - 1];
791     // System.out.println("Min value " + firstIndex);
792     // System.out.println("Max value " + lastIndex);
793     for (int x = firstIndex; x <= lastIndex; x++)
794     {
795       if (!resNumMap.containsKey(x) && !omitNonObserved.contains(x))
796       {
797         resNumMap.put(x, "-");
798       }
799     }
800   }
801
802   @Override
803   public Entity getEntityById(String id) throws SiftsException
804   {
805     // Determines an entity to process by performing a heuristic matching of all
806     // Entities with the given chainId and choosing the best matching Entity
807     Entity entity = getEntityByMostOptimalMatchedId(id);
808     if (entity != null)
809     {
810       return entity;
811     }
812     throw new SiftsException("Entity " + id + " not found");
813   }
814
815   /**
816    * This method was added because EntityId is NOT always equal to ChainId.
817    * Hence, it provides the logic to greedily detect the "true" Entity for a
818    * given chainId where discrepancies exist.
819    * 
820    * @param chainId
821    * @return
822    */
823   public Entity getEntityByMostOptimalMatchedId(String chainId)
824   {
825     // System.out.println("---> advanced greedy entityId matching block
826     // entered..");
827     List<Entity> entities = siftsEntry.getEntity();
828     SiftsEntitySortPojo[] sPojo = new SiftsEntitySortPojo[entities.size()];
829     int count = 0;
830     for (Entity entity : entities)
831     {
832       sPojo[count] = new SiftsEntitySortPojo();
833       sPojo[count].entityId = entity.getEntityId();
834
835       List<Segment> segments = entity.getSegment();
836       for (Segment segment : segments)
837       {
838         List<Residue> residues = segment.getListResidue().getResidue();
839         for (Residue residue : residues)
840         {
841           List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
842           for (CrossRefDb cRefDb : cRefDbs)
843           {
844             if (!cRefDb.getDbSource().equalsIgnoreCase("PDB"))
845             {
846               continue;
847             }
848             ++sPojo[count].resCount;
849             if (cRefDb.getDbChainId().equalsIgnoreCase(chainId))
850             {
851               ++sPojo[count].chainIdFreq;
852             }
853           }
854         }
855       }
856       sPojo[count].pid = (100 * sPojo[count].chainIdFreq)
857               / sPojo[count].resCount;
858       ++count;
859     }
860     Arrays.sort(sPojo, Collections.reverseOrder());
861     // System.out.println("highest matched entity : " + sPojo[0].entityId);
862     // System.out.println("highest matched pid : " + sPojo[0].pid);
863
864     if (sPojo[0].entityId != null)
865     {
866       if (sPojo[0].pid < 1)
867       {
868         return null;
869       }
870       for (Entity entity : entities)
871       {
872         if (!entity.getEntityId().equalsIgnoreCase(sPojo[0].entityId))
873         {
874           continue;
875         }
876         return entity;
877       }
878     }
879     return null;
880   }
881
882   private class SiftsEntitySortPojo
883           implements Comparable<SiftsEntitySortPojo>
884   {
885     public String entityId;
886
887     public int chainIdFreq;
888
889     public int pid;
890
891     public int resCount;
892
893     @Override
894     public int compareTo(SiftsEntitySortPojo o)
895     {
896       return this.pid - o.pid;
897     }
898   }
899
900   private class SegmentHelperPojo
901   {
902     private SequenceI seq;
903
904     private HashMap<Integer, int[]> mapping;
905
906     private TreeMap<Integer, String> resNumMap;
907
908     private List<Integer> omitNonObserved;
909
910     private int nonObservedShiftIndex;
911
912     public SegmentHelperPojo(SequenceI seq, HashMap<Integer, int[]> mapping,
913             TreeMap<Integer, String> resNumMap,
914             List<Integer> omitNonObserved, int nonObservedShiftIndex)
915     {
916       setSeq(seq);
917       setMapping(mapping);
918       setResNumMap(resNumMap);
919       setOmitNonObserved(omitNonObserved);
920       setNonObservedShiftIndex(nonObservedShiftIndex);
921     }
922
923     public SequenceI getSeq()
924     {
925       return seq;
926     }
927
928     public void setSeq(SequenceI seq)
929     {
930       this.seq = seq;
931     }
932
933     public HashMap<Integer, int[]> getMapping()
934     {
935       return mapping;
936     }
937
938     public void setMapping(HashMap<Integer, int[]> mapping)
939     {
940       this.mapping = mapping;
941     }
942
943     public TreeMap<Integer, String> getResNumMap()
944     {
945       return resNumMap;
946     }
947
948     public void setResNumMap(TreeMap<Integer, String> resNumMap)
949     {
950       this.resNumMap = resNumMap;
951     }
952
953     public List<Integer> getOmitNonObserved()
954     {
955       return omitNonObserved;
956     }
957
958     public void setOmitNonObserved(List<Integer> omitNonObserved)
959     {
960       this.omitNonObserved = omitNonObserved;
961     }
962
963     public int getNonObservedShiftIndex()
964     {
965       return nonObservedShiftIndex;
966     }
967
968     public void setNonObservedShiftIndex(int nonObservedShiftIndex)
969     {
970       this.nonObservedShiftIndex = nonObservedShiftIndex;
971     }
972   }
973
974   @Override
975   public StringBuilder getMappingOutput(MappingOutputPojo mp)
976           throws SiftsException
977   {
978     String seqRes = mp.getSeqResidue();
979     String seqName = mp.getSeqName();
980     int sStart = mp.getSeqStart();
981     int sEnd = mp.getSeqEnd();
982
983     String strRes = mp.getStrResidue();
984     String strName = mp.getStrName();
985     int pdbStart = mp.getStrStart();
986     int pdbEnd = mp.getStrEnd();
987
988     String type = mp.getType();
989
990     int maxid = (seqName.length() >= strName.length()) ? seqName.length()
991             : strName.length();
992     int len = 72 - maxid - 1;
993
994     int nochunks = ((seqRes.length()) / len)
995             + ((seqRes.length()) % len > 0 ? 1 : 0);
996     // output mappings
997     StringBuilder output = new StringBuilder(512);
998     output.append(NEWLINE);
999     output.append("Sequence \u27f7 Structure mapping details")
1000             .append(NEWLINE);
1001     output.append("Method: SIFTS");
1002     output.append(NEWLINE).append(NEWLINE);
1003
1004     output.append(new Format("%" + maxid + "s").form(seqName));
1005     output.append(" :  ");
1006     output.append(String.valueOf(sStart));
1007     output.append(" - ");
1008     output.append(String.valueOf(sEnd));
1009     output.append(" Maps to ");
1010     output.append(NEWLINE);
1011     output.append(new Format("%" + maxid + "s").form(structId));
1012     output.append(" :  ");
1013     output.append(String.valueOf(pdbStart));
1014     output.append(" - ");
1015     output.append(String.valueOf(pdbEnd));
1016     output.append(NEWLINE).append(NEWLINE);
1017
1018     ScoreMatrix pam250 = ScoreModels.getInstance().getPam250();
1019     int matchedSeqCount = 0;
1020     for (int j = 0; j < nochunks; j++)
1021     {
1022       // Print the first aligned sequence
1023       output.append(new Format("%" + (maxid) + "s").form(seqName))
1024               .append(" ");
1025
1026       for (int i = 0; i < len; i++)
1027       {
1028         if ((i + (j * len)) < seqRes.length())
1029         {
1030           output.append(seqRes.charAt(i + (j * len)));
1031         }
1032       }
1033
1034       output.append(NEWLINE);
1035       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
1036
1037       /*
1038        * Print out the match symbols:
1039        * | for exact match (ignoring case)
1040        * . if PAM250 score is positive
1041        * else a space
1042        */
1043       for (int i = 0; i < len; i++)
1044       {
1045         try
1046         {
1047           if ((i + (j * len)) < seqRes.length())
1048           {
1049             char c1 = seqRes.charAt(i + (j * len));
1050             char c2 = strRes.charAt(i + (j * len));
1051             boolean sameChar = Comparison.isSameResidue(c1, c2, false);
1052             if (sameChar && !Comparison.isGap(c1))
1053             {
1054               matchedSeqCount++;
1055               output.append("|");
1056             }
1057             else if (type.equals("pep"))
1058             {
1059               if (pam250.getPairwiseScore(c1, c2) > 0)
1060               {
1061                 output.append(".");
1062               }
1063               else
1064               {
1065                 output.append(" ");
1066               }
1067             }
1068             else
1069             {
1070               output.append(" ");
1071             }
1072           }
1073         } catch (IndexOutOfBoundsException e)
1074         {
1075           continue;
1076         }
1077       }
1078       // Now print the second aligned sequence
1079       output = output.append(NEWLINE);
1080       output = output.append(new Format("%" + (maxid) + "s").form(strName))
1081               .append(" ");
1082       for (int i = 0; i < len; i++)
1083       {
1084         if ((i + (j * len)) < strRes.length())
1085         {
1086           output.append(strRes.charAt(i + (j * len)));
1087         }
1088       }
1089       output.append(NEWLINE).append(NEWLINE);
1090     }
1091     float pid = (float) matchedSeqCount / seqRes.length() * 100;
1092     if (pid < SiftsSettings.getFailSafePIDThreshold())
1093     {
1094       throw new SiftsException(">>> Low PID detected for SIFTs mapping...");
1095     }
1096     output.append("Length of alignment = " + seqRes.length())
1097             .append(NEWLINE);
1098     output.append(new Format("Percentage ID = %2.2f").form(pid));
1099     return output;
1100   }
1101
1102   @Override
1103   public int getEntityCount()
1104   {
1105     return siftsEntry.getEntity().size();
1106   }
1107
1108   @Override
1109   public String getDbAccessionId()
1110   {
1111     return siftsEntry.getDbAccessionId();
1112   }
1113
1114   @Override
1115   public String getDbCoordSys()
1116   {
1117     return siftsEntry.getDbCoordSys();
1118   }
1119
1120   @Override
1121   public String getDbSource()
1122   {
1123     return siftsEntry.getDbSource();
1124   }
1125
1126   @Override
1127   public String getDbVersion()
1128   {
1129     return siftsEntry.getDbVersion();
1130   }
1131
1132   public static void setMockSiftsFile(File file)
1133   {
1134     mockSiftsFile = file;
1135   }
1136
1137 }