JAL-3438 spotless for 2.11.2.0
[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_FTP_BASE_URL = "http://ftp.ebi.ac.uk/pub/databases/msd/sifts/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 = SIFTS_FTP_BASE_URL + 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   /**
352    * Delete the SIFTs file for the given PDB Id in the local SIFTs download
353    * directory
354    * 
355    * @param pdbId
356    * @return true if the file was deleted or doesn't exist
357    */
358   public static boolean deleteSiftsFileByPDBId(String pdbId)
359   {
360     File siftsFile = new File(SiftsSettings.getSiftDownloadDirectory()
361             + pdbId.toLowerCase(Locale.ROOT) + ".xml.gz");
362     if (siftsFile.exists())
363     {
364       return siftsFile.delete();
365     }
366     return true;
367   }
368
369   /**
370    * Get a valid SIFTs DBRef for the given sequence current SIFTs entry
371    * 
372    * @param seq
373    *          - the target sequence for the operation
374    * @return a valid DBRefEntry that is SIFTs compatible
375    * @throws Exception
376    *           if no valid source DBRefEntry was found for the given sequences
377    */
378   public DBRefEntryI getValidSourceDBRef(SequenceI seq)
379           throws SiftsException
380   {
381     List<DBRefEntry> dbRefs = seq.getPrimaryDBRefs();
382     if (dbRefs == null || dbRefs.size() < 1)
383     {
384       throw new SiftsException(
385               "Source DBRef could not be determined. DBRefs might not have been retrieved.");
386     }
387
388     for (DBRefEntry dbRef : dbRefs)
389     {
390       if (dbRef == null || dbRef.getAccessionId() == null
391               || dbRef.getSource() == null)
392       {
393         continue;
394       }
395       String canonicalSource = DBRefUtils
396               .getCanonicalName(dbRef.getSource());
397       if (isValidDBRefEntry(dbRef)
398               && (canonicalSource.equalsIgnoreCase(DBRefSource.UNIPROT)
399                       || canonicalSource.equalsIgnoreCase(DBRefSource.PDB)))
400       {
401         return dbRef;
402       }
403     }
404     throw new SiftsException("Could not get source DB Ref");
405   }
406
407   /**
408    * Check that the DBRef Entry is properly populated and is available in this
409    * SiftClient instance
410    * 
411    * @param entry
412    *          - DBRefEntry to validate
413    * @return true validation is successful otherwise false is returned.
414    */
415   boolean isValidDBRefEntry(DBRefEntryI entry)
416   {
417     return entry != null && entry.getAccessionId() != null
418             && isFoundInSiftsEntry(entry.getAccessionId());
419   }
420
421   @Override
422   public HashSet<String> getAllMappingAccession()
423   {
424     HashSet<String> accessions = new HashSet<String>();
425     List<Entity> entities = siftsEntry.getEntity();
426     for (Entity entity : entities)
427     {
428       List<Segment> segments = entity.getSegment();
429       for (Segment segment : segments)
430       {
431         List<MapRegion> mapRegions = segment.getListMapRegion()
432                 .getMapRegion();
433         for (MapRegion mapRegion : mapRegions)
434         {
435           accessions.add(mapRegion.getDb().getDbAccessionId()
436                   .toLowerCase(Locale.ROOT));
437         }
438       }
439     }
440     return accessions;
441   }
442
443   @Override
444   public StructureMapping getSiftsStructureMapping(SequenceI seq,
445           String pdbFile, String chain) throws SiftsException
446   {
447     SequenceI aseq = seq;
448     while (seq.getDatasetSequence() != null)
449     {
450       seq = seq.getDatasetSequence();
451     }
452     structId = (chain == null) ? pdbId : pdbId + "|" + chain;
453     System.out.println("Getting SIFTS mapping for " + structId + ": seq "
454             + seq.getName());
455
456     final StringBuilder mappingDetails = new StringBuilder(128);
457     PrintStream ps = new PrintStream(System.out)
458     {
459       @Override
460       public void print(String x)
461       {
462         mappingDetails.append(x);
463       }
464
465       @Override
466       public void println()
467       {
468         mappingDetails.append(NEWLINE);
469       }
470     };
471     HashMap<Integer, int[]> mapping = getGreedyMapping(chain, seq, ps);
472
473     String mappingOutput = mappingDetails.toString();
474     StructureMapping siftsMapping = new StructureMapping(aseq, pdbFile,
475             pdbId, chain, mapping, mappingOutput, seqFromPdbMapping);
476
477     return siftsMapping;
478   }
479
480   @Override
481   public HashMap<Integer, int[]> getGreedyMapping(String entityId,
482           SequenceI seq, java.io.PrintStream os) throws SiftsException
483   {
484     List<Integer> omitNonObserved = new ArrayList<>();
485     int nonObservedShiftIndex = 0, pdbeNonObserved = 0;
486     // System.out.println("Generating mappings for : " + entityId);
487     Entity entity = null;
488     entity = getEntityById(entityId);
489     String originalSeq = AlignSeq.extractGaps(
490             jalview.util.Comparison.GapChars, seq.getSequenceAsString());
491     HashMap<Integer, int[]> mapping = new HashMap<Integer, int[]>();
492     DBRefEntryI sourceDBRef;
493     sourceDBRef = getValidSourceDBRef(seq);
494     // TODO ensure sequence start/end is in the same coordinate system and
495     // consistent with the choosen sourceDBRef
496
497     // set sequence coordinate system - default value is UniProt
498     if (sourceDBRef.getSource().equalsIgnoreCase(DBRefSource.PDB))
499     {
500       seqCoordSys = CoordinateSys.PDB;
501     }
502
503     HashSet<String> dbRefAccessionIdsString = new HashSet<String>();
504     for (DBRefEntry dbref : seq.getDBRefs())
505     {
506       dbRefAccessionIdsString
507               .add(dbref.getAccessionId().toLowerCase(Locale.ROOT));
508     }
509     dbRefAccessionIdsString
510             .add(sourceDBRef.getAccessionId().toLowerCase(Locale.ROOT));
511
512     curDBRefAccessionIdsString = dbRefAccessionIdsString;
513     curSourceDBRef = sourceDBRef.getAccessionId();
514
515     TreeMap<Integer, String> resNumMap = new TreeMap<Integer, String>();
516     List<Segment> segments = entity.getSegment();
517     SegmentHelperPojo shp = new SegmentHelperPojo(seq, mapping, resNumMap,
518             omitNonObserved, nonObservedShiftIndex, pdbeNonObserved);
519     processSegments(segments, shp);
520     try
521     {
522       populateAtomPositions(entityId, mapping);
523     } catch (Exception e)
524     {
525       e.printStackTrace();
526     }
527     if (seqCoordSys == CoordinateSys.UNIPROT)
528     {
529       padWithGaps(resNumMap, omitNonObserved);
530     }
531     int seqStart = UNASSIGNED;
532     int seqEnd = UNASSIGNED;
533     int pdbStart = UNASSIGNED;
534     int pdbEnd = UNASSIGNED;
535
536     if (mapping.isEmpty())
537     {
538       throw new SiftsException("SIFTS mapping failed");
539     }
540     // also construct a mapping object between the seq-coord sys and the PDB
541     // seq's coord sys
542
543     Integer[] keys = mapping.keySet().toArray(new Integer[0]);
544     Arrays.sort(keys);
545     seqStart = keys[0];
546     seqEnd = keys[keys.length - 1];
547     List<int[]> from = new ArrayList<>(), to = new ArrayList<>();
548     int[] _cfrom = null, _cto = null;
549     String matchedSeq = originalSeq;
550     if (seqStart != UNASSIGNED) // fixme! seqStart can map to -1 for a pdb
551                                 // sequence that starts <-1
552     {
553       for (int seqps : keys)
554       {
555         int pdbpos = mapping.get(seqps)[PDBE_POS];
556         if (pdbpos == UNASSIGNED)
557         {
558           // not correct - pdbpos might be -1, but leave it for now
559           continue;
560         }
561         if (_cfrom == null || seqps != _cfrom[1] + 1)
562         {
563           _cfrom = new int[] { seqps, seqps };
564           from.add(_cfrom);
565           _cto = null; // discontinuity
566         }
567         else
568         {
569           _cfrom[1] = seqps;
570         }
571         if (_cto == null || pdbpos != 1 + _cto[1])
572         {
573           _cto = new int[] { pdbpos, pdbpos };
574           to.add(_cto);
575         }
576         else
577         {
578           _cto[1] = pdbpos;
579         }
580       }
581       _cfrom = new int[from.size() * 2];
582       _cto = new int[to.size() * 2];
583       int p = 0;
584       for (int[] range : from)
585       {
586         _cfrom[p++] = range[0];
587         _cfrom[p++] = range[1];
588       }
589       ;
590       p = 0;
591       for (int[] range : to)
592       {
593         _cto[p++] = range[0];
594         _cto[p++] = range[1];
595       }
596       ;
597
598       seqFromPdbMapping = new jalview.datamodel.Mapping(null, _cto, _cfrom,
599               1, 1);
600       pdbStart = mapping.get(seqStart)[PDB_RES_POS];
601       pdbEnd = mapping.get(seqEnd)[PDB_RES_POS];
602       int orignalSeqStart = seq.getStart();
603       if (orignalSeqStart >= 1)
604       {
605         int subSeqStart = (seqStart >= orignalSeqStart)
606                 ? seqStart - orignalSeqStart
607                 : 0;
608         int subSeqEnd = seqEnd - (orignalSeqStart - 1);
609         subSeqEnd = originalSeq.length() < subSeqEnd ? originalSeq.length()
610                 : subSeqEnd;
611         matchedSeq = originalSeq.substring(subSeqStart, subSeqEnd);
612       }
613       else
614       {
615         matchedSeq = originalSeq.substring(1, originalSeq.length());
616       }
617     }
618
619     StringBuilder targetStrucSeqs = new StringBuilder();
620     for (String res : resNumMap.values())
621     {
622       targetStrucSeqs.append(res);
623     }
624
625     if (os != null)
626     {
627       MappingOutputPojo mop = new MappingOutputPojo();
628       mop.setSeqStart(seqStart);
629       mop.setSeqEnd(seqEnd);
630       mop.setSeqName(seq.getName());
631       mop.setSeqResidue(matchedSeq);
632
633       mop.setStrStart(pdbStart);
634       mop.setStrEnd(pdbEnd);
635       mop.setStrName(structId);
636       mop.setStrResidue(targetStrucSeqs.toString());
637
638       mop.setType("pep");
639       os.print(getMappingOutput(mop).toString());
640       os.println();
641     }
642     return mapping;
643   }
644
645   void processSegments(List<Segment> segments, SegmentHelperPojo shp)
646   {
647     SequenceI seq = shp.getSeq();
648     HashMap<Integer, int[]> mapping = shp.getMapping();
649     TreeMap<Integer, String> resNumMap = shp.getResNumMap();
650     List<Integer> omitNonObserved = shp.getOmitNonObserved();
651     int nonObservedShiftIndex = shp.getNonObservedShiftIndex();
652     int pdbeNonObservedCount = shp.getPdbeNonObserved();
653     int firstPDBResNum = UNASSIGNED;
654     for (Segment segment : segments)
655     {
656       // System.out.println("Mapping segments : " + segment.getSegId() + "\\"s
657       // + segStartEnd);
658       List<Residue> residues = segment.getListResidue().getResidue();
659       for (Residue residue : residues)
660       {
661         boolean isObserved = isResidueObserved(residue);
662         int pdbeIndex = getLeadingIntegerValue(residue.getDbResNum(),
663                 UNASSIGNED);
664         int currSeqIndex = UNASSIGNED;
665         List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
666         CrossRefDb pdbRefDb = null;
667         for (CrossRefDb cRefDb : cRefDbs)
668         {
669           if (cRefDb.getDbSource().equalsIgnoreCase(DBRefSource.PDB))
670           {
671             pdbRefDb = cRefDb;
672             if (firstPDBResNum == UNASSIGNED)
673             {
674               firstPDBResNum = getLeadingIntegerValue(cRefDb.getDbResNum(),
675                       UNASSIGNED);
676             }
677             else
678             {
679               if (isObserved)
680               {
681                 // after we find the first observed residue we just increment
682                 firstPDBResNum++;
683               }
684             }
685           }
686           if (cRefDb.getDbCoordSys().equalsIgnoreCase(seqCoordSys.getName())
687                   && isAccessionMatched(cRefDb.getDbAccessionId()))
688           {
689             currSeqIndex = getLeadingIntegerValue(cRefDb.getDbResNum(),
690                     UNASSIGNED);
691             if (pdbRefDb != null)
692             {
693               break;// exit loop if pdb and uniprot are already found
694             }
695           }
696         }
697         if (!isObserved)
698         {
699           ++pdbeNonObservedCount;
700         }
701         if (seqCoordSys == seqCoordSys.PDB) // FIXME: is seqCoordSys ever PDBe
702                                             // ???
703         {
704           // if the sequence has a primary reference to the PDB, then we are
705           // dealing with a sequence extracted directly from the PDB. In that
706           // case, numbering is PDBe - non-observed residues
707           currSeqIndex = seq.getStart() - 1 + pdbeIndex;
708         }
709         if (!isObserved)
710         {
711           if (seqCoordSys != CoordinateSys.UNIPROT) // FIXME: PDB or PDBe only
712                                                     // here
713           {
714             // mapping to PDB or PDBe so we need to bookkeep for the
715             // non-observed
716             // SEQRES positions
717             omitNonObserved.add(currSeqIndex);
718             ++nonObservedShiftIndex;
719           }
720         }
721         if (currSeqIndex == UNASSIGNED)
722         {
723           // change in logic - unobserved residues with no currSeqIndex
724           // corresponding are still counted in both nonObservedShiftIndex and
725           // pdbeIndex...
726           continue;
727         }
728         // if (currSeqIndex >= seq.getStart() && currSeqIndex <= seqlength) //
729         // true
730         // numbering
731         // is
732         // not
733         // up
734         // to
735         // seq.getEnd()
736         {
737
738           int resNum = (pdbRefDb == null)
739                   ? getLeadingIntegerValue(residue.getDbResNum(),
740                           UNASSIGNED)
741                   : getLeadingIntegerValue(pdbRefDb.getDbResNum(),
742                           UNASSIGNED);
743
744           if (isObserved)
745           {
746             char resCharCode = ResidueProperties
747                     .getSingleCharacterCode(ResidueProperties
748                             .getCanonicalAminoAcid(residue.getDbResName()));
749             resNumMap.put(currSeqIndex, String.valueOf(resCharCode));
750
751             int[] mappingcols = new int[] { Integer.valueOf(resNum),
752                 UNASSIGNED, isObserved ? firstPDBResNum : UNASSIGNED };
753
754             mapping.put(currSeqIndex - nonObservedShiftIndex, mappingcols);
755           }
756         }
757       }
758     }
759   }
760
761   /**
762    * Get the leading integer part of a string that begins with an integer.
763    * 
764    * @param input
765    *          - the string input to process
766    * @param failValue
767    *          - value returned if unsuccessful
768    * @return
769    */
770   static int getLeadingIntegerValue(String input, int failValue)
771   {
772     if (input == null)
773     {
774       return failValue;
775     }
776     String[] parts = input.split("(?=\\D)(?<=\\d)");
777     if (parts != null && parts.length > 0 && parts[0].matches("[0-9]+"))
778     {
779       return Integer.valueOf(parts[0]);
780     }
781     return failValue;
782   }
783
784   /**
785    * 
786    * @param chainId
787    *          Target chain to populate mapping of its atom positions.
788    * @param mapping
789    *          Two dimension array of residue index versus atom position
790    * @throws IllegalArgumentException
791    *           Thrown if chainId or mapping is null
792    * @throws SiftsException
793    */
794   void populateAtomPositions(String chainId, Map<Integer, int[]> mapping)
795           throws IllegalArgumentException, SiftsException
796   {
797     try
798     {
799       PDBChain chain = pdb.findChain(chainId);
800
801       if (chain == null || mapping == null)
802       {
803         throw new IllegalArgumentException(
804                 "Chain id or mapping must not be null.");
805       }
806       for (int[] map : mapping.values())
807       {
808         if (map[PDB_RES_POS] != UNASSIGNED)
809         {
810           map[PDB_ATOM_POS] = getAtomIndex(map[PDB_RES_POS], chain.atoms);
811         }
812       }
813     } catch (NullPointerException e)
814     {
815       throw new SiftsException(e.getMessage());
816     } catch (Exception e)
817     {
818       throw new SiftsException(e.getMessage());
819     }
820   }
821
822   /**
823    * 
824    * @param residueIndex
825    *          The residue index used for the search
826    * @param atoms
827    *          A collection of Atom to search
828    * @return atom position for the given residue index
829    */
830   int getAtomIndex(int residueIndex, Collection<Atom> atoms)
831   {
832     if (atoms == null)
833     {
834       throw new IllegalArgumentException(
835               "atoms collection must not be null!");
836     }
837     for (Atom atom : atoms)
838     {
839       if (atom.resNumber == residueIndex)
840       {
841         return atom.atomIndex;
842       }
843     }
844     return UNASSIGNED;
845   }
846
847   /**
848    * Checks if the residue instance is marked 'Not_observed' or not
849    * 
850    * @param residue
851    * @return
852    */
853   private boolean isResidueObserved(Residue residue)
854   {
855     Set<String> annotations = getResidueAnnotaitons(residue,
856             ResidueDetailType.ANNOTATION);
857     if (annotations == null || annotations.isEmpty())
858     {
859       return true;
860     }
861     for (String annotation : annotations)
862     {
863       if (annotation.equalsIgnoreCase(NOT_OBSERVED))
864       {
865         return false;
866       }
867     }
868     return true;
869   }
870
871   /**
872    * Get annotation String for a given residue and annotation type
873    * 
874    * @param residue
875    * @param type
876    * @return
877    */
878   private Set<String> getResidueAnnotaitons(Residue residue,
879           ResidueDetailType type)
880   {
881     HashSet<String> foundAnnotations = new HashSet<String>();
882     List<ResidueDetail> resDetails = residue.getResidueDetail();
883     for (ResidueDetail resDetail : resDetails)
884     {
885       if (resDetail.getProperty().equalsIgnoreCase(type.getCode()))
886       {
887         foundAnnotations.add(resDetail.getContent());
888       }
889     }
890     return foundAnnotations;
891   }
892
893   @Override
894   public boolean isAccessionMatched(String accession)
895   {
896     boolean isStrictMatch = true;
897     return isStrictMatch ? curSourceDBRef.equalsIgnoreCase(accession)
898             : curDBRefAccessionIdsString
899                     .contains(accession.toLowerCase(Locale.ROOT));
900   }
901
902   private boolean isFoundInSiftsEntry(String accessionId)
903   {
904     Set<String> siftsDBRefs = getAllMappingAccession();
905     return accessionId != null
906             && siftsDBRefs.contains(accessionId.toLowerCase(Locale.ROOT));
907   }
908
909   /**
910    * Pad omitted residue positions in PDB sequence with gaps
911    * 
912    * @param resNumMap
913    */
914   void padWithGaps(Map<Integer, String> resNumMap,
915           List<Integer> omitNonObserved)
916   {
917     if (resNumMap == null || resNumMap.isEmpty())
918     {
919       return;
920     }
921     Integer[] keys = resNumMap.keySet().toArray(new Integer[0]);
922     // Arrays.sort(keys);
923     int firstIndex = keys[0];
924     int lastIndex = keys[keys.length - 1];
925     // System.out.println("Min value " + firstIndex);
926     // System.out.println("Max value " + lastIndex);
927     for (int x = firstIndex; x <= lastIndex; x++)
928     {
929       if (!resNumMap.containsKey(x) && !omitNonObserved.contains(x))
930       {
931         resNumMap.put(x, "-");
932       }
933     }
934   }
935
936   @Override
937   public Entity getEntityById(String id) throws SiftsException
938   {
939     // Determines an entity to process by performing a heuristic matching of all
940     // Entities with the given chainId and choosing the best matching Entity
941     Entity entity = getEntityByMostOptimalMatchedId(id);
942     if (entity != null)
943     {
944       return entity;
945     }
946     throw new SiftsException("Entity " + id + " not found");
947   }
948
949   /**
950    * This method was added because EntityId is NOT always equal to ChainId.
951    * Hence, it provides the logic to greedily detect the "true" Entity for a
952    * given chainId where discrepancies exist.
953    * 
954    * @param chainId
955    * @return
956    */
957   public Entity getEntityByMostOptimalMatchedId(String chainId)
958   {
959     // System.out.println("---> advanced greedy entityId matching block
960     // entered..");
961     List<Entity> entities = siftsEntry.getEntity();
962     SiftsEntitySortPojo[] sPojo = new SiftsEntitySortPojo[entities.size()];
963     int count = 0;
964     for (Entity entity : entities)
965     {
966       sPojo[count] = new SiftsEntitySortPojo();
967       sPojo[count].entityId = entity.getEntityId();
968
969       List<Segment> segments = entity.getSegment();
970       for (Segment segment : segments)
971       {
972         List<Residue> residues = segment.getListResidue().getResidue();
973         for (Residue residue : residues)
974         {
975           List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
976           for (CrossRefDb cRefDb : cRefDbs)
977           {
978             if (!cRefDb.getDbSource().equalsIgnoreCase("PDB"))
979             {
980               continue;
981             }
982             ++sPojo[count].resCount;
983             if (cRefDb.getDbChainId().equalsIgnoreCase(chainId))
984             {
985               ++sPojo[count].chainIdFreq;
986             }
987           }
988         }
989       }
990       sPojo[count].pid = (100 * sPojo[count].chainIdFreq)
991               / sPojo[count].resCount;
992       ++count;
993     }
994     Arrays.sort(sPojo, Collections.reverseOrder());
995     // System.out.println("highest matched entity : " + sPojo[0].entityId);
996     // System.out.println("highest matched pid : " + sPojo[0].pid);
997
998     if (sPojo[0].entityId != null)
999     {
1000       if (sPojo[0].pid < 1)
1001       {
1002         return null;
1003       }
1004       for (Entity entity : entities)
1005       {
1006         if (!entity.getEntityId().equalsIgnoreCase(sPojo[0].entityId))
1007         {
1008           continue;
1009         }
1010         return entity;
1011       }
1012     }
1013     return null;
1014   }
1015
1016   private class SiftsEntitySortPojo
1017           implements Comparable<SiftsEntitySortPojo>
1018   {
1019     public String entityId;
1020
1021     public int chainIdFreq;
1022
1023     public int pid;
1024
1025     public int resCount;
1026
1027     @Override
1028     public int compareTo(SiftsEntitySortPojo o)
1029     {
1030       return this.pid - o.pid;
1031     }
1032   }
1033
1034   private class SegmentHelperPojo
1035   {
1036     private SequenceI seq;
1037
1038     private HashMap<Integer, int[]> mapping;
1039
1040     private TreeMap<Integer, String> resNumMap;
1041
1042     private List<Integer> omitNonObserved;
1043
1044     private int nonObservedShiftIndex;
1045
1046     /**
1047      * count of number of 'not observed' positions in the PDB record's SEQRES
1048      * (total number of residues with coordinates == length(SEQRES) -
1049      * pdbeNonObserved
1050      */
1051     private int pdbeNonObserved;
1052
1053     public SegmentHelperPojo(SequenceI seq, HashMap<Integer, int[]> mapping,
1054             TreeMap<Integer, String> resNumMap,
1055             List<Integer> omitNonObserved, int nonObservedShiftIndex,
1056             int pdbeNonObserved)
1057     {
1058       setSeq(seq);
1059       setMapping(mapping);
1060       setResNumMap(resNumMap);
1061       setOmitNonObserved(omitNonObserved);
1062       setNonObservedShiftIndex(nonObservedShiftIndex);
1063       setPdbeNonObserved(pdbeNonObserved);
1064
1065     }
1066
1067     public void setPdbeNonObserved(int pdbeNonObserved2)
1068     {
1069       this.pdbeNonObserved = pdbeNonObserved2;
1070     }
1071
1072     public int getPdbeNonObserved()
1073     {
1074       return pdbeNonObserved;
1075     }
1076
1077     public SequenceI getSeq()
1078     {
1079       return seq;
1080     }
1081
1082     public void setSeq(SequenceI seq)
1083     {
1084       this.seq = seq;
1085     }
1086
1087     public HashMap<Integer, int[]> getMapping()
1088     {
1089       return mapping;
1090     }
1091
1092     public void setMapping(HashMap<Integer, int[]> mapping)
1093     {
1094       this.mapping = mapping;
1095     }
1096
1097     public TreeMap<Integer, String> getResNumMap()
1098     {
1099       return resNumMap;
1100     }
1101
1102     public void setResNumMap(TreeMap<Integer, String> resNumMap)
1103     {
1104       this.resNumMap = resNumMap;
1105     }
1106
1107     public List<Integer> getOmitNonObserved()
1108     {
1109       return omitNonObserved;
1110     }
1111
1112     public void setOmitNonObserved(List<Integer> omitNonObserved)
1113     {
1114       this.omitNonObserved = omitNonObserved;
1115     }
1116
1117     public int getNonObservedShiftIndex()
1118     {
1119       return nonObservedShiftIndex;
1120     }
1121
1122     public void setNonObservedShiftIndex(int nonObservedShiftIndex)
1123     {
1124       this.nonObservedShiftIndex = nonObservedShiftIndex;
1125     }
1126
1127   }
1128
1129   @Override
1130   public StringBuilder getMappingOutput(MappingOutputPojo mp)
1131           throws SiftsException
1132   {
1133     String seqRes = mp.getSeqResidue();
1134     String seqName = mp.getSeqName();
1135     int sStart = mp.getSeqStart();
1136     int sEnd = mp.getSeqEnd();
1137
1138     String strRes = mp.getStrResidue();
1139     String strName = mp.getStrName();
1140     int pdbStart = mp.getStrStart();
1141     int pdbEnd = mp.getStrEnd();
1142
1143     String type = mp.getType();
1144
1145     int maxid = (seqName.length() >= strName.length()) ? seqName.length()
1146             : strName.length();
1147     int len = 72 - maxid - 1;
1148
1149     int nochunks = ((seqRes.length()) / len)
1150             + ((seqRes.length()) % len > 0 ? 1 : 0);
1151     // output mappings
1152     StringBuilder output = new StringBuilder(512);
1153     output.append(NEWLINE);
1154     output.append("Sequence \u27f7 Structure mapping details")
1155             .append(NEWLINE);
1156     output.append("Method: SIFTS");
1157     output.append(NEWLINE).append(NEWLINE);
1158
1159     output.append(new Format("%" + maxid + "s").form(seqName));
1160     output.append(" :  ");
1161     output.append(String.valueOf(sStart));
1162     output.append(" - ");
1163     output.append(String.valueOf(sEnd));
1164     output.append(" Maps to ");
1165     output.append(NEWLINE);
1166     output.append(new Format("%" + maxid + "s").form(structId));
1167     output.append(" :  ");
1168     output.append(String.valueOf(pdbStart));
1169     output.append(" - ");
1170     output.append(String.valueOf(pdbEnd));
1171     output.append(NEWLINE).append(NEWLINE);
1172
1173     ScoreMatrix pam250 = ScoreModels.getInstance().getPam250();
1174     int matchedSeqCount = 0;
1175     for (int j = 0; j < nochunks; j++)
1176     {
1177       // Print the first aligned sequence
1178       output.append(new Format("%" + (maxid) + "s").form(seqName))
1179               .append(" ");
1180
1181       for (int i = 0; i < len; i++)
1182       {
1183         if ((i + (j * len)) < seqRes.length())
1184         {
1185           output.append(seqRes.charAt(i + (j * len)));
1186         }
1187       }
1188
1189       output.append(NEWLINE);
1190       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
1191
1192       /*
1193        * Print out the match symbols:
1194        * | for exact match (ignoring case)
1195        * . if PAM250 score is positive
1196        * else a space
1197        */
1198       for (int i = 0; i < len; i++)
1199       {
1200         try
1201         {
1202           if ((i + (j * len)) < seqRes.length())
1203           {
1204             char c1 = seqRes.charAt(i + (j * len));
1205             char c2 = strRes.charAt(i + (j * len));
1206             boolean sameChar = Comparison.isSameResidue(c1, c2, false);
1207             if (sameChar && !Comparison.isGap(c1))
1208             {
1209               matchedSeqCount++;
1210               output.append("|");
1211             }
1212             else if (type.equals("pep"))
1213             {
1214               if (pam250.getPairwiseScore(c1, c2) > 0)
1215               {
1216                 output.append(".");
1217               }
1218               else
1219               {
1220                 output.append(" ");
1221               }
1222             }
1223             else
1224             {
1225               output.append(" ");
1226             }
1227           }
1228         } catch (IndexOutOfBoundsException e)
1229         {
1230           continue;
1231         }
1232       }
1233       // Now print the second aligned sequence
1234       output = output.append(NEWLINE);
1235       output = output.append(new Format("%" + (maxid) + "s").form(strName))
1236               .append(" ");
1237       for (int i = 0; i < len; i++)
1238       {
1239         if ((i + (j * len)) < strRes.length())
1240         {
1241           output.append(strRes.charAt(i + (j * len)));
1242         }
1243       }
1244       output.append(NEWLINE).append(NEWLINE);
1245     }
1246     float pid = (float) matchedSeqCount / seqRes.length() * 100;
1247     if (pid < SiftsSettings.getFailSafePIDThreshold())
1248     {
1249       throw new SiftsException(">>> Low PID detected for SIFTs mapping...");
1250     }
1251     output.append("Length of alignment = " + seqRes.length())
1252             .append(NEWLINE);
1253     output.append(new Format("Percentage ID = %2.2f").form(pid));
1254     return output;
1255   }
1256
1257   @Override
1258   public int getEntityCount()
1259   {
1260     return siftsEntry.getEntity().size();
1261   }
1262
1263   @Override
1264   public String getDbAccessionId()
1265   {
1266     return siftsEntry.getDbAccessionId();
1267   }
1268
1269   @Override
1270   public String getDbCoordSys()
1271   {
1272     return siftsEntry.getDbCoordSys();
1273   }
1274
1275   @Override
1276   public String getDbSource()
1277   {
1278     return siftsEntry.getDbSource();
1279   }
1280
1281   @Override
1282   public String getDbVersion()
1283   {
1284     return siftsEntry.getDbVersion();
1285   }
1286
1287   public static void setMockSiftsFile(File file)
1288   {
1289     mockSiftsFile = file;
1290   }
1291
1292 }