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