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