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