1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (2.11.1.4)
3  * Copyright (C) 2021 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.jws1;
22 
23 import jalview.analysis.AlignSeq;
24 import jalview.api.FeatureColourI;
25 import jalview.bin.Cache;
26 import jalview.datamodel.Alignment;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.AlignmentView;
29 import jalview.datamodel.SequenceI;
30 import jalview.gui.AlignFrame;
31 import jalview.gui.Desktop;
32 import jalview.gui.WebserviceInfo;
33 import jalview.io.NewickFile;
34 import jalview.util.MessageManager;
35 import jalview.ws.AWsJob;
36 import jalview.ws.JobStateSummary;
37 import jalview.ws.WSClientI;
38 
39 import java.util.HashMap;
40 import java.util.Hashtable;
41 import java.util.Map;
42 import java.util.Vector;
43 
44 import vamsas.objects.simple.MsaResult;
45 import vamsas.objects.simple.SeqSearchResult;
46 
47 class SeqSearchWSThread extends JWS1Thread implements WSClientI
48 {
49   String dbs = null;
50 
51   boolean profile = false;
52 
53   class SeqSearchWSJob extends WSJob
54   {
55     // hold special input for this
56     vamsas.objects.simple.SequenceSet seqs = new vamsas.objects.simple.SequenceSet();
57 
58     /**
59      * MsaWSJob
60      *
61      * @param jobNum
62      *          int
63      * @param jobId
64      *          String
65      */
SeqSearchWSJob(int jobNum, SequenceI[] inSeqs)66     public SeqSearchWSJob(int jobNum, SequenceI[] inSeqs)
67     {
68       this.jobnum = jobNum;
69       if (!prepareInput(inSeqs, 2))
70       {
71         submitted = true;
72         subjobComplete = true;
73         result = new MsaResult();
74         result.setFinished(true);
75         result.setStatus(MessageManager.getString("label.job_never_ran"));
76       }
77 
78     }
79 
80     Hashtable SeqNames = new Hashtable();
81 
82     Vector emptySeqs = new Vector();
83 
84     /**
85      * prepare input sequences for service
86      *
87      * @param seqs
88      *          jalview sequences to be prepared
89      * @param minlen
90      *          minimum number of residues required for this MsaWS service
91      * @return true if seqs contains sequences to be submitted to service.
92      */
prepareInput(SequenceI[] seqs, int minlen)93     private boolean prepareInput(SequenceI[] seqs, int minlen)
94     {
95       int nseqs = 0;
96       if (minlen < 0)
97       {
98         throw new Error(MessageManager.getString(
99                 "error.implementation_error_minlen_must_be_greater_zero"));
100       }
101       for (int i = 0; i < seqs.length; i++)
102       {
103         if (seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
104         {
105           nseqs++;
106         }
107       }
108       boolean valid = nseqs >= 1; // need at least one sequence for valid input
109       // TODO: generalise
110       vamsas.objects.simple.Sequence[] seqarray = (valid)
111               ? new vamsas.objects.simple.Sequence[nseqs]
112               : null;
113       boolean submitGaps = (nseqs == 1) ? false : true; // profile is submitted
114       // with gaps
115       for (int i = 0, n = 0; i < seqs.length; i++)
116       {
117 
118         String newname = jalview.analysis.SeqsetUtils.unique_name(i); // same
119         // for
120         // any
121         // subjob
122         SeqNames.put(newname,
123                 jalview.analysis.SeqsetUtils.SeqCharacterHash(seqs[i]));
124         if (valid && seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
125         {
126           seqarray[n] = new vamsas.objects.simple.Sequence();
127           seqarray[n].setId(newname);
128           seqarray[n++].setSeq((submitGaps) ? seqs[i].getSequenceAsString()
129                   : AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
130                           seqs[i].getSequenceAsString()));
131         }
132         else
133         {
134           String empty = null;
135           if (seqs[i].getEnd() >= seqs[i].getStart())
136           {
137             empty = (submitGaps) ? seqs[i].getSequenceAsString()
138                     : AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
139                             seqs[i].getSequenceAsString());
140           }
141           emptySeqs.add(new String[] { newname, empty });
142         }
143       }
144       if (submitGaps)
145       {
146         // almost certainly have to remove gapped columns here
147       }
148       this.seqs = new vamsas.objects.simple.SequenceSet();
149       this.seqs.setSeqs(seqarray);
150       return valid;
151     }
152 
153     /**
154      *
155      * @return true if getAlignment will return a valid alignment result.
156      */
157     @Override
hasResults()158     public boolean hasResults()
159     {
160       if (subjobComplete && result != null && result.isFinished()
161               && ((SeqSearchResult) result).getAlignment() != null
162               && ((SeqSearchResult) result).getAlignment()
163                       .getSeqs() != null)
164       {
165         return true;
166       }
167       return false;
168     }
169 
170     /**
171      * return sequence search results for display
172      *
173      * @return null or { Alignment(+features and annotation), NewickFile)}
174      */
getAlignment(AlignmentI dataset, Map<String, FeatureColourI> featureColours)175     public Object[] getAlignment(AlignmentI dataset,
176             Map<String, FeatureColourI> featureColours)
177     {
178 
179       if (result != null && result.isFinished())
180       {
181         SequenceI[] alseqs = null;
182         // char alseq_gapchar = '-';
183         // int alseq_l = 0;
184         if (((SeqSearchResult) result).getAlignment() != null)
185         {
186           alseqs = getVamsasAlignment(
187                   ((SeqSearchResult) result).getAlignment());
188           // alseq_gapchar = ( (SeqSearchResult)
189           // result).getAlignment().getGapchar().charAt(0);
190           // alseq_l = alseqs.length;
191         }
192         /**
193          * what has to be done. 1 - annotate returned alignment with annotation
194          * file and sequence features file, and associate any tree-nodes. 2.
195          * connect alignment back to any associated dataset: 2.a. deuniquify
196          * recovers sequence information - but additionally, relocations must be
197          * made from the returned aligned sequence back to the dataset.
198          */
199         // construct annotated alignment as it would be done by the jalview
200         // applet
201         jalview.datamodel.Alignment al = new Alignment(alseqs);
202         // al.setDataset(dataset);
203         // make dataset
204         String inFile = null;
205         try
206         {
207           inFile = ((SeqSearchResult) result).getAnnotation();
208           if (inFile != null && inFile.length() > 0)
209           {
210             new jalview.io.AnnotationFile().readAnnotationFile(al, inFile,
211                     jalview.io.DataSourceType.PASTE);
212           }
213         } catch (Exception e)
214         {
215           System.err.println(
216                   "Failed to parse the annotation file associated with the alignment.");
217           System.err.println(">>>EOF" + inFile + "\n<<<EOF\n");
218           e.printStackTrace(System.err);
219         }
220 
221         try
222         {
223           inFile = ((SeqSearchResult) result).getFeatures();
224           if (inFile != null && inFile.length() > 0)
225           {
226             jalview.io.FeaturesFile ff = new jalview.io.FeaturesFile(inFile,
227                     jalview.io.DataSourceType.PASTE);
228             ff.parse(al, featureColours, false);
229           }
230         } catch (Exception e)
231         {
232           System.err.println(
233                   "Failed to parse the Features file associated with the alignment.");
234           System.err.println(">>>EOF" + inFile + "\n<<<EOF\n");
235           e.printStackTrace(System.err);
236         }
237         jalview.io.NewickFile nf = null;
238         try
239         {
240           inFile = ((SeqSearchResult) result).getNewickTree();
241           if (inFile != null && inFile.length() > 0)
242           {
243             nf = new jalview.io.NewickFile(inFile,
244                     jalview.io.DataSourceType.PASTE);
245             if (!nf.isValid())
246             {
247               nf.close();
248               nf = null;
249             }
250           }
251         } catch (Exception e)
252         {
253           System.err.println(
254                   "Failed to parse the treeFile associated with the alignment.");
255           System.err.println(">>>EOF" + inFile + "\n<<<EOF\n");
256           e.printStackTrace(System.err);
257         }
258 
259         /*
260          * TODO: housekeeping w.r.t. recovery of dataset and annotation
261          * references for input sequences, and then dataset sequence creation
262          * for new sequences retrieved from service // finally, attempt to
263          * de-uniquify to recover input sequence identity, and try to map back
264          * onto dataset Note: this
265          * jalview.analysis.SeqsetUtils.deuniquify(SeqNames, alseqs, true); will
266          * NOT WORK - the returned alignment may contain multiple versions of
267          * the input sequence, each being a subsequence of the original.
268          * deuniquify also removes existing annotation and features added in the
269          * previous step... al.setDataset(dataset); // add in new sequences
270          * retrieved from sequence search which are not already in dataset. //
271          * trigger a 'fetchDBids' to annotate sequences with database ids...
272          */
273 
274         return new Object[] { al, nf };
275       }
276       return null;
277     }
278 
279     /**
280      * mark subjob as cancelled and set result object appropriatly
281      */
cancel()282     void cancel()
283     {
284       cancelled = true;
285       subjobComplete = true;
286       result = null;
287     }
288 
289     /**
290      *
291      * @return boolean true if job can be submitted.
292      */
293     @Override
hasValidInput()294     public boolean hasValidInput()
295     {
296       if (seqs.getSeqs() != null)
297       {
298         return true;
299       }
300       return false;
301     }
302   }
303 
304   String alTitle; // name which will be used to form new alignment window.
305 
306   AlignmentI dataset; // dataset to which the new alignment will be
307 
308   // associated.
309 
310   ext.vamsas.SeqSearchI server = null;
311 
312   private String dbArg;
313 
314   /**
315    * set basic options for this (group) of Msa jobs
316    *
317    * @param subgaps
318    *          boolean
319    * @param presorder
320    *          boolean
321    */
SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl, WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame, AlignmentView alview, String wsname, String db)322   SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl,
323           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
324           AlignmentView alview, String wsname, String db)
325   {
326     super(alFrame, wsinfo, alview, wsname, wsUrl);
327     this.server = server;
328     this.dbArg = db;
329   }
330 
331   /**
332    * create one or more Msa jobs to align visible seuqences in _msa
333    *
334    * @param title
335    *          String
336    * @param _msa
337    *          AlignmentView
338    * @param subgaps
339    *          boolean
340    * @param presorder
341    *          boolean
342    * @param seqset
343    *          Alignment
344    */
SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl, WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame, String wsname, String title, AlignmentView _msa, String db, AlignmentI seqset)345   SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl,
346           WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame,
347           String wsname, String title, AlignmentView _msa, String db,
348           AlignmentI seqset)
349   {
350     this(server, wsUrl, wsinfo, alFrame, _msa, wsname, db);
351     OutputHeader = wsInfo.getProgressText();
352     alTitle = title;
353     dataset = seqset;
354 
355     SequenceI[][] conmsa = _msa.getVisibleContigs('-');
356     if (conmsa != null)
357     {
358       int njobs = conmsa.length;
359       jobs = new SeqSearchWSJob[njobs];
360       for (int j = 0; j < njobs; j++)
361       {
362         if (j != 0)
363         {
364           jobs[j] = new SeqSearchWSJob(wsinfo.addJobPane(), conmsa[j]);
365         }
366         else
367         {
368           jobs[j] = new SeqSearchWSJob(0, conmsa[j]);
369         }
370         if (njobs > 0)
371         {
372           wsinfo.setProgressName("region " + jobs[j].getJobnum(),
373                   jobs[j].getJobnum());
374         }
375         wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
376       }
377     }
378   }
379 
380   @Override
isCancellable()381   public boolean isCancellable()
382   {
383     return true;
384   }
385 
386   @Override
cancelJob()387   public void cancelJob()
388   {
389     if (!jobComplete && jobs != null)
390     {
391       boolean cancelled = true;
392       for (int job = 0; job < jobs.length; job++)
393       {
394         if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete())
395         {
396           String cancelledMessage = "";
397           try
398           {
399             vamsas.objects.simple.WsJobId cancelledJob = server
400                     .cancel(jobs[job].getJobId());
401             if (cancelledJob.getStatus() == 2)
402             {
403               // CANCELLED_JOB
404               cancelledMessage = "Job cancelled.";
405               ((SeqSearchWSJob) jobs[job]).cancel();
406               wsInfo.setStatus(jobs[job].getJobnum(),
407                       WebserviceInfo.STATE_CANCELLED_OK);
408             }
409             else if (cancelledJob.getStatus() == 3)
410             {
411               // VALID UNSTOPPABLE JOB
412               cancelledMessage += "Server cannot cancel this job. just close the window.\n";
413               cancelled = false;
414               // wsInfo.setStatus(jobs[job].jobnum,
415               // WebserviceInfo.STATE_RUNNING);
416             }
417 
418             if (cancelledJob.getJobId() != null)
419             {
420               cancelledMessage += ("[" + cancelledJob.getJobId() + "]");
421             }
422 
423             cancelledMessage += "\n";
424           } catch (Exception exc)
425           {
426             cancelledMessage += ("\nProblems cancelling the job : Exception received...\n"
427                     + exc + "\n");
428             Cache.log.warn(
429                     "Exception whilst cancelling " + jobs[job].getJobId(),
430                     exc);
431           }
432           wsInfo.setProgressText(jobs[job].getJobnum(),
433                   OutputHeader + cancelledMessage + "\n");
434         }
435       }
436       if (cancelled)
437       {
438         wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
439         jobComplete = true;
440       }
441       this.interrupt(); // kick thread to update job states.
442     }
443     else
444     {
445       if (!jobComplete)
446       {
447         wsInfo.setProgressText(OutputHeader
448                 + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n");
449       }
450     }
451   }
452 
453   @Override
pollJob(AWsJob job)454   public void pollJob(AWsJob job) throws Exception
455   {
456     ((SeqSearchWSJob) job).result = server
457             .getResult(((SeqSearchWSJob) job).getJobId());
458   }
459 
460   @Override
StartJob(AWsJob job)461   public void StartJob(AWsJob job)
462   {
463     if (!(job instanceof SeqSearchWSJob))
464     {
465       throw new Error(MessageManager.formatMessage(
466               "error.implementation_error_msawbjob_called", new String[]
467               { job.getClass().toString() }));
468     }
469     SeqSearchWSJob j = (SeqSearchWSJob) job;
470     if (j.isSubmitted())
471     {
472       if (Cache.log.isDebugEnabled())
473       {
474         Cache.log.debug(
475                 "Tried to submit an already submitted job " + j.getJobId());
476       }
477       return;
478     }
479     if (j.seqs.getSeqs() == null)
480     {
481       // special case - selection consisted entirely of empty sequences...
482       j.setSubmitted(true);
483       j.result = new MsaResult();
484       j.result.setFinished(true);
485       j.result.setStatus(
486               MessageManager.getString("label.empty_alignment_job"));
487       ((MsaResult) j.result).setMsa(null);
488     }
489     try
490     {
491       vamsas.objects.simple.WsJobId jobsubmit = server
492               .search(j.seqs.getSeqs()[0], dbArg);
493 
494       if ((jobsubmit != null) && (jobsubmit.getStatus() == 1))
495       {
496         j.setJobId(jobsubmit.getJobId());
497         j.setSubmitted(true);
498         j.setSubjobComplete(false);
499         // System.out.println(WsURL + " Job Id '" + jobId + "'");
500       }
501       else
502       {
503         if (jobsubmit == null)
504         {
505           throw new Exception(MessageManager.formatMessage(
506                   "exception.web_service_returned_null_try_later",
507                   new String[]
508                   { WsUrl }));
509         }
510 
511         throw new Exception(jobsubmit.getJobId());
512       }
513     } catch (Exception e)
514     {
515       // TODO: JBPNote catch timeout or other fault types explicitly
516       // For unexpected errors
517       System.err.println(WebServiceName
518               + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n"
519               + "When contacting Server:" + WsUrl + "\n" + e.toString()
520               + "\n");
521       j.setAllowedServerExceptions(0);
522       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
523       wsInfo.setStatus(j.getJobnum(),
524               WebserviceInfo.STATE_STOPPED_SERVERERROR);
525       wsInfo.appendProgressText(j.getJobnum(), MessageManager
526               .getString("info.failed_to_submit_sequences_for_alignment"));
527 
528       // e.printStackTrace(); // TODO: JBPNote DEBUG
529     }
530   }
531 
getVamsasAlignment( vamsas.objects.simple.Alignment valign)532   private jalview.datamodel.Sequence[] getVamsasAlignment(
533           vamsas.objects.simple.Alignment valign)
534   {
535     vamsas.objects.simple.Sequence[] seqs = valign.getSeqs().getSeqs();
536     jalview.datamodel.Sequence[] msa = new jalview.datamodel.Sequence[seqs.length];
537 
538     for (int i = 0, j = seqs.length; i < j; i++)
539     {
540       msa[i] = new jalview.datamodel.Sequence(seqs[i].getId(),
541               seqs[i].getSeq());
542     }
543 
544     return msa;
545   }
546 
547   @Override
parseResult()548   public void parseResult()
549   {
550     int results = 0; // number of result sets received
551     JobStateSummary finalState = new JobStateSummary();
552     try
553     {
554       for (int j = 0; j < jobs.length; j++)
555       {
556         finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
557         if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete()
558                 && jobs[j].hasResults())
559         {
560           results++;
561           vamsas.objects.simple.Alignment valign = ((SeqSearchResult) ((SeqSearchWSJob) jobs[j]).result)
562                   .getAlignment();
563           if (valign != null)
564           {
565             wsInfo.appendProgressText(jobs[j].getJobnum(), MessageManager
566                     .getString("info.alignment_object_method_notes"));
567             String[] lines = valign.getMethod();
568             for (int line = 0; line < lines.length; line++)
569             {
570               wsInfo.appendProgressText(jobs[j].getJobnum(),
571                       lines[line] + "\n");
572             }
573             // JBPNote The returned files from a webservice could be
574             // hidden behind icons in the monitor window that,
575             // when clicked, pop up their corresponding data
576           }
577         }
578       }
579     } catch (Exception ex)
580     {
581 
582       Cache.log.error(
583               "Unexpected exception when processing results for " + alTitle,
584               ex);
585       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
586     }
587     if (results > 0)
588     {
589       wsInfo.showResultsNewFrame
590               .addActionListener(new java.awt.event.ActionListener()
591               {
592                 @Override
593                 public void actionPerformed(java.awt.event.ActionEvent evt)
594                 {
595                   displayResults(true);
596                 }
597               });
598       wsInfo.mergeResults
599               .addActionListener(new java.awt.event.ActionListener()
600               {
601                 @Override
602                 public void actionPerformed(java.awt.event.ActionEvent evt)
603                 {
604                   displayResults(false);
605                 }
606               });
607       wsInfo.setResultsReady();
608     }
609     else
610     {
611       wsInfo.setFinishedNoResults();
612     }
613   }
614 
displayResults(boolean newFrame)615   void displayResults(boolean newFrame)
616   {
617     if (!newFrame)
618     {
619       System.err.println("MERGE WITH OLD FRAME NOT IMPLEMENTED");
620       return;
621     }
622     // each subjob is an independent alignment for the moment
623     // Alignment al[] = new Alignment[jobs.length];
624     // NewickFile nf[] = new NewickFile[jobs.length];
625     for (int j = 0; j < jobs.length; j++)
626     {
627       Map<String, FeatureColourI> featureColours = new HashMap<String, FeatureColourI>();
628       Alignment al = null;
629       NewickFile nf = null;
630       if (jobs[j].hasResults())
631       {
632         Object[] res = ((SeqSearchWSJob) jobs[j]).getAlignment(dataset,
633                 featureColours);
634         if (res == null)
635         {
636           continue;
637         }
638         ;
639         al = (Alignment) res[0];
640         nf = (NewickFile) res[1];
641       }
642       else
643       {
644         al = null;
645         nf = null;
646         continue;
647       }
648       /*
649        * We can't map new alignment back with insertions from input's hidden
650        * regions until dataset mapping is sorted out... but basically it goes
651        * like this: 1. Merge each domain hit back onto the visible segments in
652        * the same way as a Jnet prediction is mapped back
653        *
654        * Object[] newview = input.getUpdatedView(results, orders, getGapChar());
655        * // trash references to original result data for (int j = 0; j <
656        * jobs.length; j++) { results[j] = null; orders[j] = null; } SequenceI[]
657        * alignment = (SequenceI[]) newview[0]; ColumnSelection columnselection =
658        * (ColumnSelection) newview[1]; Alignment al = new Alignment(alignment);
659        *
660        * if (dataset != null) { al.setDataset(dataset); }
661        *
662        * propagateDatasetMappings(al); }
663        */
664 
665       AlignFrame af = new AlignFrame(al, // columnselection,
666               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
667       if (nf != null)
668       {
669         af.showNewickTree(nf,
670                 MessageManager.formatMessage("label.tree_from", new String[]
671                 { this.alTitle }));
672       }
673       // initialise with same renderer settings as in parent alignframe.
674       af.getFeatureRenderer().transferSettings(this.featureSettings);
675       Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
676               AlignFrame.DEFAULT_HEIGHT);
677     }
678   }
679 
680   @Override
canMergeResults()681   public boolean canMergeResults()
682   {
683     return false;
684   }
685 }
686