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