1f0a6022ce9d0a75597bd0642de6a72981900fea
[jalview.git] / src / jalview / ws2 / operations / AlignmentOperation.java
1 package jalview.ws2.operations;
2
3 import static java.lang.String.format;
4
5 import java.awt.event.MouseAdapter;
6 import java.awt.event.MouseEvent;
7 import java.io.IOException;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.HashMap;
11 import java.util.Hashtable;
12 import java.util.LinkedHashMap;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Objects;
16 import java.util.concurrent.CompletionStage;
17 import java.util.function.Consumer;
18
19 import javax.swing.JMenu;
20 import javax.swing.JMenuItem;
21 import javax.swing.ToolTipManager;
22
23 import org.eclipse.jetty.http.HttpGenerator.Result;
24
25 import jalview.analysis.AlignSeq;
26 import jalview.analysis.AlignmentSorter;
27 import jalview.analysis.SeqsetUtils;
28 import jalview.bin.Cache;
29 import jalview.datamodel.AlignedCodonFrame;
30 import jalview.datamodel.Alignment;
31 import jalview.datamodel.AlignmentI;
32 import jalview.datamodel.AlignmentOrder;
33 import jalview.datamodel.AlignmentView;
34 import jalview.datamodel.HiddenColumns;
35 import jalview.datamodel.SequenceI;
36 import jalview.datamodel.Sequence;
37 import jalview.gui.AlignFrame;
38 import jalview.gui.AlignViewport;
39 import jalview.gui.Desktop;
40 import jalview.gui.JvSwingUtils;
41 import jalview.gui.WebserviceInfo;
42 import jalview.gui.WsJobParameters;
43 import jalview.util.MathUtils;
44 import jalview.util.MessageManager;
45 import jalview.ws.params.ArgumentI;
46 import jalview.ws.params.ParamDatastoreI;
47 import jalview.ws.params.WsParamSetI;
48 import jalview.ws2.MenuEntryProviderI;
49 import jalview.ws2.ResultSupplier;
50 import jalview.ws2.WSJob;
51 import jalview.ws2.WSJobStatus;
52 import jalview.ws2.PollingTaskExecutor;
53 import jalview.ws2.WebServiceI;
54 import jalview.ws2.WebServiceInfoUpdater;
55 import jalview.ws2.WebServiceWorkerI;
56 import jalview.ws2.WebServiceWorkerListener;
57 import jalview.ws2.WebServiceWorkerListenersList;
58 import jalview.ws2.gui.AlignmentMenuBuilder;
59 import jalview.ws2.utils.WSJobList;
60
61 /**
62  *
63  * @author mmwarowny
64  *
65  */
66 public class AlignmentOperation implements Operation
67 {
68   private final WebServiceI service;
69
70   private final ResultSupplier<AlignmentI> supplier;
71   
72
73   public AlignmentOperation(
74       WebServiceI service,
75       ResultSupplier<AlignmentI> supplier)
76   {
77     this.service = service;
78     this.supplier = supplier;
79   }
80
81   @Override
82   public String getName()
83   {
84     return service.getName();
85   }
86   
87   @Override
88   public String getDescription()
89   {
90     return service.getDescription();
91   }
92
93   @Override
94   public String getTypeName()
95   {
96     return "Multiple Sequence Alignment";
97   }
98
99   @Override
100   public String getHostName()
101   {
102     return service.getHostName();
103   }
104   
105   @Override
106   public boolean hasParameters()
107   {
108     return service.hasParameters();
109   }
110   
111   @Override
112   public ParamDatastoreI getParamStore()
113   {
114     return service.getParamStore();
115   }
116
117   @Override
118   public int getMinSequences()
119   {
120     return 2;
121   }
122
123   @Override
124   public int getMaxSequences()
125   {
126     return Integer.MAX_VALUE;
127   }
128
129   @Override
130   public boolean isProteinOperation()
131   {
132     return true;
133   }
134
135   @Override
136   public boolean isNucleotideOperation()
137   {
138     return true;
139   }
140   
141   @Override
142   public boolean isAlignmentAnalysis()
143   {
144     return false;
145   }
146
147   @Override
148   public boolean canSubmitGaps()
149   {
150     // hack copied from original jabaws code, don't blame me
151     return getName().contains("lustal");
152   }
153
154   @Override
155   public boolean isInteractive()
156   {
157     return false;
158   }
159   
160   @Override
161   public boolean getFilterNonStandardSymbols()
162   {
163     return true;
164   }
165   
166   @Override
167   public boolean getNeedsAlignedSequences()
168   {
169     return false;
170   }
171
172   @Override
173   public MenuEntryProviderI getMenuBuilder()
174   {
175     return new AlignmentMenuBuilder(this);
176   }
177
178
179   /**
180    * Implementation of the web service worker performing multiple sequence
181    * alignment.
182    *
183    * @author mmwarowny
184    *
185    */
186   public class AlignmentWorker implements WebServiceWorkerI
187   {
188
189     private long uid = MathUtils.getUID();
190
191     private final AlignmentView msa;
192
193     private final AlignmentI dataset;
194
195     private final AlignViewport viewport;
196
197     private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
198
199     private List<ArgumentI> args = Collections.emptyList();
200
201     private String alnTitle = "";
202
203     private boolean submitGaps = false;
204
205     private boolean preserveOrder = false;
206
207     private char gapCharacter;
208
209     private WSJobList jobs = new WSJobList();
210
211     private Map<Long, JobInput> inputs = new LinkedHashMap<>();
212
213     private Map<Long, Integer> exceptionCount = new HashMap<>();
214     
215     private final int MAX_RETRY = 5;
216
217     public AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
218             String alnTitle, boolean submitGaps, boolean preserveOrder,
219             AlignViewport viewport)
220     {
221       this.msa = msa;
222       this.dataset = viewport.getAlignment().getDataset();
223       List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
224           viewport.getAlignment().getCodonFrames(), Collections.emptyList());
225       this.codonFrame.addAll(cf);
226       this.args = args;
227       this.alnTitle = alnTitle;
228       this.submitGaps = submitGaps;
229       this.preserveOrder = preserveOrder;
230       this.viewport = viewport;
231       this.gapCharacter = viewport.getGapCharacter();
232     }
233
234     @Override
235     public long getUID()
236     {
237       return uid;
238     }
239
240     @Override
241     public WebServiceI getWebService()
242     {
243       return service;
244     }
245
246     @Override
247     public WSJobList getJobs()
248     {
249       return jobs;
250     }
251
252     @Override
253     public void start() throws IOException
254     {
255       Cache.log.info(format("Starting new %s job.", service.getName()));
256       SequenceI[][] conmsa = msa.getVisibleContigs('-');
257       if (conmsa == null)
258       {
259         return;
260       }
261       int numValid = 0;
262       for (int i = 0; i < conmsa.length; i++)
263       {
264         JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
265         WSJob job = new WSJob(service.getProviderName(), service.getName(),
266                 service.getHostName());
267         job.setJobNum(i);
268         inputs.put(job.getUid(), input);
269         jobs.add(job);
270         listeners.fireJobCreated(job);
271         if (input.isInputValid())
272         {
273           int count;
274           String jobId = null;
275           do
276           {
277             count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
278             try
279             {
280               jobId = service.submit(input.inputSequences, args);
281               Cache.log.debug((format("Job %s submitted", job)));
282               exceptionCount.remove(job.getUid());
283             } catch (IOException e)
284             {
285               exceptionCount.put(job.getUid(), --count);
286             }
287           } while (jobId == null && count > 0);
288           if (jobId != null)
289           {
290             job.setJobId(jobId);
291             job.setStatus(WSJobStatus.SUBMITTED);
292             numValid++;
293           }
294           else
295           {
296             job.setStatus(WSJobStatus.SERVER_ERROR);
297           }
298         }
299         else
300         {
301           job.setStatus(WSJobStatus.INVALID);
302           job.setErrorLog(
303                   MessageManager.getString("label.empty_alignment_job"));
304         }
305       }
306       if (numValid > 0)
307       {
308         listeners.fireWorkerStarted();
309       }
310       else
311       {
312         listeners.fireWorkerNotStarted();
313       }
314     }
315
316     @Override
317     public boolean poll()
318     {
319       boolean done = true;
320       for (WSJob job : getJobs())
321       {
322         if (!job.getStatus().isDone() && !job.getStatus().isFailed())
323         {
324           Cache.log.debug(format("Polling job %s.", job));
325           try
326           {
327             service.updateProgress(job);
328             exceptionCount.remove(job.getUid());
329           } catch (IOException e)
330           {
331             Cache.log.error(format("Polling job %s failed.", job), e);
332             listeners.firePollException(job, e);
333             int count = exceptionCount.getOrDefault(job.getUid(),
334                     MAX_RETRY);
335             if (--count <= 0)
336             {
337               job.setStatus(WSJobStatus.SERVER_ERROR);
338               Cache.log.warn(format(
339                       "Attempts limit exceeded. Droping job %s.", job));
340             }
341             exceptionCount.put(job.getUid(), count);
342           } catch (OutOfMemoryError e)
343           {
344             job.setStatus(WSJobStatus.BROKEN);
345             Cache.log.error(
346                     format("Out of memory when retrieving job %s", job), e);
347           }
348           Cache.log.debug(
349                   format("Job %s status is %s", job, job.getStatus()));
350         }
351         done &= job.getStatus().isDone() || job.getStatus().isFailed();
352       }
353       return done;
354     }
355
356
357     @Override
358     public void done()
359     {
360       listeners.fireWorkerCompleting();
361       Map<Long, AlignmentI> results = new LinkedHashMap<>();
362       for (WSJob job : getJobs())
363       {
364         if (job.getStatus().isFailed())
365           continue;
366         try
367         {
368           AlignmentI alignment = supplier.getResult(job, 
369               dataset.getSequences(), viewport);
370           if (alignment != null)
371           {
372             results.put(job.getUid(), alignment);
373           }
374         } catch (Exception e)
375         {
376           if (!service.handleCollectionError(job, e))
377           {
378             Cache.log.error("Couldn't get alignment for job.", e);
379             // TODO: Increment exception count and retry.
380             job.setStatus(WSJobStatus.SERVER_ERROR);
381           }
382         }
383       }
384       if (results.size() > 0)
385       {
386         AlignmentResult out = prepareResult(results);
387         resultConsumer.accept(out);
388       }
389       else
390       {
391         resultConsumer.accept(null);
392       }
393       listeners.fireWorkerCompleted();
394     }
395
396     private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
397     {
398       List<AlignmentOrder> alorders = new ArrayList<>();
399       SequenceI[][] results = new SequenceI[jobs.size()][];
400       AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
401       for (int i = 0; i < jobs.size(); i++)
402       {
403         WSJob job = jobs.get(i);
404         AlignmentI aln = alignments.get(job.getUid());
405         if (aln != null) // equivalent of job.hasResults()
406         {
407           /* Get the alignment including any empty sequences in the original
408            * order with original ids. */
409           JobInput input = inputs.get(job.getUid());
410           char gapChar = aln.getGapCharacter();
411           List<SequenceI> emptySeqs = input.emptySequences;
412           List<SequenceI> alnSeqs = aln.getSequences();
413           // find the width of the longest sequence
414           int width = 0;
415           for (var seq : alnSeqs)
416             width = Integer.max(width, seq.getLength());
417           for (var emptySeq : emptySeqs)
418             width = Integer.max(width, emptySeq.getLength());
419           // pad shorter sequences with gaps
420           String gapSeq = String.join("",
421                   Collections.nCopies(width, Character.toString(gapChar)));
422           List<SequenceI> seqs = new ArrayList<>(
423                   alnSeqs.size() + emptySeqs.size());
424           seqs.addAll(alnSeqs);
425           seqs.addAll(emptySeqs);
426           for (var seq : seqs)
427           {
428             if (seq.getLength() < width)
429               seq.setSequence(seq.getSequenceAsString()
430                       + gapSeq.substring(seq.getLength()));
431           }
432           SequenceI[] result = seqs.toArray(new SequenceI[0]);
433           AlignmentOrder msaOrder = new AlignmentOrder(result);
434           AlignmentSorter.recoverOrder(result);
435           // temporary workaround for deuniquify
436           @SuppressWarnings({ "rawtypes", "unchecked" })
437           Hashtable names = new Hashtable(input.sequenceNames);
438           // FIXME first call to deuniquify alters original alignment
439           SeqsetUtils.deuniquify(names, result);
440           alorders.add(msaOrder);
441           results[i] = result;
442           orders[i] = msaOrder;
443         }
444         else
445         {
446           results[i] = null;
447         }
448       }
449
450       Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
451       // free references to original data
452       for (int i = 0; i < jobs.size(); i++)
453       {
454         results[i] = null;
455         orders[i] = null;
456       }
457       SequenceI[] alignment = (SequenceI[]) newView[0];
458       HiddenColumns hidden = (HiddenColumns) newView[1];
459       Alignment aln = new Alignment(alignment);
460       aln.setProperty("Alignment Program", service.getName());
461       if (dataset != null)
462         aln.setDataset(dataset);
463
464       propagateDatasetMappings(aln);
465       return new AlignmentResult(aln, alorders, hidden);
466       // displayNewFrame(aln, alorders, hidden);
467     }
468
469     /*
470      * conserves dataset references to sequence objects returned from web
471      * services. propagate codon frame data to alignment.
472      */
473     private void propagateDatasetMappings(Alignment aln)
474     {
475       if (codonFrame != null)
476       {
477         SequenceI[] alignment = aln.getSequencesArray();
478         for (SequenceI seq : alignment)
479         {
480           for (AlignedCodonFrame acf : codonFrame)
481           {
482             if (acf != null && acf.involvesSequence(seq))
483             {
484               aln.addCodonFrame(acf);
485               break;
486             }
487           }
488         }
489       }
490     }
491     
492     private Consumer<AlignmentResult> resultConsumer;
493     
494     public void setResultConsumer(Consumer<AlignmentResult> consumer)
495     {
496       this.resultConsumer = consumer;
497     }
498
499     private WebServiceWorkerListenersList listeners = 
500         new WebServiceWorkerListenersList(this);
501     
502     @Override
503     public void addListener(WebServiceWorkerListener listener)
504     {
505       listeners.addListener(listener);
506     }
507   }
508   
509   public class AlignmentResult
510   {
511     AlignmentI aln;
512
513     List<AlignmentOrder> alorders;
514
515     HiddenColumns hidden;
516
517     AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
518             HiddenColumns hidden)
519     {
520       this.aln = aln;
521       this.alorders = alorders;
522       this.hidden = hidden;
523     }
524
525     public AlignmentI getAln()
526     {
527       return aln;
528     }
529
530     public List<AlignmentOrder> getAlorders()
531     {
532       return alorders;
533     }
534
535     public HiddenColumns getHidden()
536     {
537       return hidden;
538     }
539   }
540
541   private static class JobInput
542   {
543     final List<SequenceI> inputSequences;
544
545     final List<SequenceI> emptySequences;
546
547     @SuppressWarnings("rawtypes")
548     final Map<String, ? extends Map> sequenceNames;
549
550     private JobInput(int numSequences, List<SequenceI> inputSequences,
551             List<SequenceI> emptySequences,
552             @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
553     {
554       this.inputSequences = Collections.unmodifiableList(inputSequences);
555       this.emptySequences = Collections.unmodifiableList(emptySequences);
556       this.sequenceNames = names;
557     }
558
559     boolean isInputValid()
560     {
561       return inputSequences.size() >= 2;
562     }
563
564     static JobInput create(SequenceI[] sequences, int minLength,
565             boolean submitGaps)
566     {
567       assert minLength >= 0 : MessageManager.getString(
568               "error.implementation_error_minlen_must_be_greater_zero");
569       int numSeq = 0;
570       for (SequenceI seq : sequences)
571       {
572         if (seq.getEnd() - seq.getStart() >= minLength)
573         {
574           numSeq++;
575         }
576       }
577
578       List<SequenceI> inputSequences = new ArrayList<>();
579       List<SequenceI> emptySequences = new ArrayList<>();
580       @SuppressWarnings("rawtypes")
581       Map<String, Hashtable> names = new LinkedHashMap<>();
582       for (int i = 0; i < sequences.length; i++)
583       {
584         SequenceI seq = sequences[i];
585         String newName = SeqsetUtils.unique_name(i);
586         @SuppressWarnings("rawtypes")
587         Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
588         names.put(newName, hash);
589         if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
590         {
591           String seqString = seq.getSequenceAsString();
592           if (!submitGaps)
593           {
594             seqString = AlignSeq.extractGaps(
595                     jalview.util.Comparison.GapChars, seqString);
596           }
597           inputSequences.add(new Sequence(newName, seqString));
598         }
599         else
600         {
601           String seqString = "";
602           if (seq.getEnd() >= seq.getStart())  // true if gaps only
603           {
604             seqString = seq.getSequenceAsString();
605             if (!submitGaps)
606             {
607               seqString = AlignSeq.extractGaps(
608                       jalview.util.Comparison.GapChars, seqString);
609             }
610           }
611           emptySequences.add(new Sequence(newName, seqString));
612         }
613       }
614
615       return new JobInput(numSeq, inputSequences, emptySequences, names);
616     }
617   }
618
619 }