JAL-3878 Add javadocs to created classes and reformat code.
[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  * Implementation of the {@link Operation} for multiple sequence alignment jobs.
63  *
64  * @author mmwarowny
65  *
66  */
67 public class AlignmentOperation implements Operation
68 {
69   private final WebServiceI service;
70
71   private final ResultSupplier<AlignmentI> supplier;
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    * Implementation of the web service worker performing multiple sequence
180    * alignment.
181    *
182    * @author mmwarowny
183    *
184    */
185   public class AlignmentWorker implements WebServiceWorkerI
186   {
187
188     private long uid = MathUtils.getUID();
189
190     private final AlignmentView msa;
191
192     private final AlignmentI dataset;
193
194     private final AlignViewport viewport;
195
196     private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
197
198     private List<ArgumentI> args = Collections.emptyList();
199
200     private String alnTitle = "";
201
202     private boolean submitGaps = false;
203
204     private boolean preserveOrder = false;
205
206     private char gapCharacter;
207
208     private WSJobList jobs = new WSJobList();
209
210     private Map<Long, JobInput> inputs = new LinkedHashMap<>();
211
212     private Map<Long, Integer> exceptionCount = new HashMap<>();
213
214     private final int MAX_RETRY = 5;
215
216     public AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
217         String alnTitle, boolean submitGaps, boolean preserveOrder,
218         AlignViewport viewport)
219     {
220       this.msa = msa;
221       this.dataset = viewport.getAlignment().getDataset();
222       List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
223           viewport.getAlignment().getCodonFrames(), Collections.emptyList());
224       this.codonFrame.addAll(cf);
225       this.args = args;
226       this.alnTitle = alnTitle;
227       this.submitGaps = submitGaps;
228       this.preserveOrder = preserveOrder;
229       this.viewport = viewport;
230       this.gapCharacter = viewport.getGapCharacter();
231     }
232
233     @Override
234     public long getUID()
235     {
236       return uid;
237     }
238
239     @Override
240     public WebServiceI getWebService()
241     {
242       return service;
243     }
244
245     @Override
246     public WSJobList getJobs()
247     {
248       return jobs;
249     }
250
251     @Override
252     public void start() throws IOException
253     {
254       Cache.log.info(format("Starting new %s job.", service.getName()));
255       SequenceI[][] conmsa = msa.getVisibleContigs('-');
256       if (conmsa == null)
257       {
258         return;
259       }
260       int numValid = 0;
261       for (int i = 0; i < conmsa.length; i++)
262       {
263         JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
264         WSJob job = new WSJob(service.getProviderName(), service.getName(),
265             service.getHostName());
266         job.setJobNum(i);
267         inputs.put(job.getUid(), input);
268         jobs.add(job);
269         listeners.fireJobCreated(job);
270         if (input.isInputValid())
271         {
272           int count;
273           String jobId = null;
274           do
275           {
276             count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
277             try
278             {
279               jobId = service.submit(input.inputSequences, args);
280               Cache.log.debug((format("Job %s submitted", job)));
281               exceptionCount.remove(job.getUid());
282             } catch (IOException e)
283             {
284               exceptionCount.put(job.getUid(), --count);
285             }
286           } while (jobId == null && count > 0);
287           if (jobId != null)
288           {
289             job.setJobId(jobId);
290             job.setStatus(WSJobStatus.SUBMITTED);
291             numValid++;
292           }
293           else
294           {
295             job.setStatus(WSJobStatus.SERVER_ERROR);
296           }
297         }
298         else
299         {
300           job.setStatus(WSJobStatus.INVALID);
301           job.setErrorLog(
302               MessageManager.getString("label.empty_alignment_job"));
303         }
304       }
305       if (numValid > 0)
306       {
307         listeners.fireWorkerStarted();
308       }
309       else
310       {
311         listeners.fireWorkerNotStarted();
312       }
313     }
314
315     @Override
316     public boolean poll()
317     {
318       boolean done = true;
319       for (WSJob job : getJobs())
320       {
321         if (!job.getStatus().isDone() && !job.getStatus().isFailed())
322         {
323           Cache.log.debug(format("Polling job %s.", job));
324           try
325           {
326             service.updateProgress(job);
327             exceptionCount.remove(job.getUid());
328           } catch (IOException e)
329           {
330             Cache.log.error(format("Polling job %s failed.", job), e);
331             listeners.firePollException(job, e);
332             int count = exceptionCount.getOrDefault(job.getUid(),
333                 MAX_RETRY);
334             if (--count <= 0)
335             {
336               job.setStatus(WSJobStatus.SERVER_ERROR);
337               Cache.log.warn(format(
338                   "Attempts limit exceeded. Droping job %s.", job));
339             }
340             exceptionCount.put(job.getUid(), count);
341           } catch (OutOfMemoryError e)
342           {
343             job.setStatus(WSJobStatus.BROKEN);
344             Cache.log.error(
345                 format("Out of memory when retrieving job %s", job), e);
346           }
347           Cache.log.debug(
348               format("Job %s status is %s", job, job.getStatus()));
349         }
350         done &= job.getStatus().isDone() || job.getStatus().isFailed();
351       }
352       return done;
353     }
354
355     @Override
356     public void done()
357     {
358       listeners.fireWorkerCompleting();
359       Map<Long, AlignmentI> results = new LinkedHashMap<>();
360       for (WSJob job : getJobs())
361       {
362         if (job.getStatus().isFailed())
363           continue;
364         try
365         {
366           AlignmentI alignment = supplier.getResult(job,
367               dataset.getSequences(), viewport);
368           if (alignment != null)
369           {
370             results.put(job.getUid(), alignment);
371           }
372         } catch (Exception e)
373         {
374           if (!service.handleCollectionError(job, e))
375           {
376             Cache.log.error("Couldn't get alignment for job.", e);
377             // TODO: Increment exception count and retry.
378             job.setStatus(WSJobStatus.SERVER_ERROR);
379           }
380         }
381       }
382       if (results.size() > 0)
383       {
384         AlignmentResult out = prepareResult(results);
385         resultConsumer.accept(out);
386       }
387       else
388       {
389         resultConsumer.accept(null);
390       }
391       listeners.fireWorkerCompleted();
392     }
393
394     private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
395     {
396       List<AlignmentOrder> alorders = new ArrayList<>();
397       SequenceI[][] results = new SequenceI[jobs.size()][];
398       AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
399       for (int i = 0; i < jobs.size(); i++)
400       {
401         WSJob job = jobs.get(i);
402         AlignmentI aln = alignments.get(job.getUid());
403         if (aln != null) // equivalent of job.hasResults()
404         {
405           /* Get the alignment including any empty sequences in the original
406            * order with original ids. */
407           JobInput input = inputs.get(job.getUid());
408           char gapChar = aln.getGapCharacter();
409           List<SequenceI> emptySeqs = input.emptySequences;
410           List<SequenceI> alnSeqs = aln.getSequences();
411           // find the width of the longest sequence
412           int width = 0;
413           for (var seq : alnSeqs)
414             width = Integer.max(width, seq.getLength());
415           for (var emptySeq : emptySeqs)
416             width = Integer.max(width, emptySeq.getLength());
417           // pad shorter sequences with gaps
418           String gapSeq = String.join("",
419               Collections.nCopies(width, Character.toString(gapChar)));
420           List<SequenceI> seqs = new ArrayList<>(
421               alnSeqs.size() + emptySeqs.size());
422           seqs.addAll(alnSeqs);
423           seqs.addAll(emptySeqs);
424           for (var seq : seqs)
425           {
426             if (seq.getLength() < width)
427               seq.setSequence(seq.getSequenceAsString()
428                   + gapSeq.substring(seq.getLength()));
429           }
430           SequenceI[] result = seqs.toArray(new SequenceI[0]);
431           AlignmentOrder msaOrder = new AlignmentOrder(result);
432           AlignmentSorter.recoverOrder(result);
433           // temporary workaround for deuniquify
434           @SuppressWarnings({ "rawtypes", "unchecked" })
435           Hashtable names = new Hashtable(input.sequenceNames);
436           // FIXME first call to deuniquify alters original alignment
437           SeqsetUtils.deuniquify(names, result);
438           alorders.add(msaOrder);
439           results[i] = result;
440           orders[i] = msaOrder;
441         }
442         else
443         {
444           results[i] = null;
445         }
446       }
447
448       Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
449       // free references to original data
450       for (int i = 0; i < jobs.size(); i++)
451       {
452         results[i] = null;
453         orders[i] = null;
454       }
455       SequenceI[] alignment = (SequenceI[]) newView[0];
456       HiddenColumns hidden = (HiddenColumns) newView[1];
457       Alignment aln = new Alignment(alignment);
458       aln.setProperty("Alignment Program", service.getName());
459       if (dataset != null)
460         aln.setDataset(dataset);
461
462       propagateDatasetMappings(aln);
463       return new AlignmentResult(aln, alorders, hidden);
464       // displayNewFrame(aln, alorders, hidden);
465     }
466
467     /*
468      * conserves dataset references to sequence objects returned from web
469      * services. propagate codon frame data to alignment.
470      */
471     private void propagateDatasetMappings(Alignment aln)
472     {
473       if (codonFrame != null)
474       {
475         SequenceI[] alignment = aln.getSequencesArray();
476         for (SequenceI seq : alignment)
477         {
478           for (AlignedCodonFrame acf : codonFrame)
479           {
480             if (acf != null && acf.involvesSequence(seq))
481             {
482               aln.addCodonFrame(acf);
483               break;
484             }
485           }
486         }
487       }
488     }
489
490     private Consumer<AlignmentResult> resultConsumer;
491
492     public void setResultConsumer(Consumer<AlignmentResult> consumer)
493     {
494       this.resultConsumer = consumer;
495     }
496
497     private WebServiceWorkerListenersList listeners = new WebServiceWorkerListenersList(this);
498
499     @Override
500     public void addListener(WebServiceWorkerListener listener)
501     {
502       listeners.addListener(listener);
503     }
504   }
505
506   public class AlignmentResult
507   {
508     AlignmentI aln;
509
510     List<AlignmentOrder> alorders;
511
512     HiddenColumns hidden;
513
514     AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
515         HiddenColumns hidden)
516     {
517       this.aln = aln;
518       this.alorders = alorders;
519       this.hidden = hidden;
520     }
521
522     public AlignmentI getAln()
523     {
524       return aln;
525     }
526
527     public List<AlignmentOrder> getAlorders()
528     {
529       return alorders;
530     }
531
532     public HiddenColumns getHidden()
533     {
534       return hidden;
535     }
536   }
537
538   private static class JobInput
539   {
540     final List<SequenceI> inputSequences;
541
542     final List<SequenceI> emptySequences;
543
544     @SuppressWarnings("rawtypes")
545     final Map<String, ? extends Map> sequenceNames;
546
547     private JobInput(int numSequences, List<SequenceI> inputSequences,
548         List<SequenceI> emptySequences,
549         @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
550     {
551       this.inputSequences = Collections.unmodifiableList(inputSequences);
552       this.emptySequences = Collections.unmodifiableList(emptySequences);
553       this.sequenceNames = names;
554     }
555
556     boolean isInputValid()
557     {
558       return inputSequences.size() >= 2;
559     }
560
561     static JobInput create(SequenceI[] sequences, int minLength,
562         boolean submitGaps)
563     {
564       assert minLength >= 0 : MessageManager.getString(
565           "error.implementation_error_minlen_must_be_greater_zero");
566       int numSeq = 0;
567       for (SequenceI seq : sequences)
568       {
569         if (seq.getEnd() - seq.getStart() >= minLength)
570         {
571           numSeq++;
572         }
573       }
574
575       List<SequenceI> inputSequences = new ArrayList<>();
576       List<SequenceI> emptySequences = new ArrayList<>();
577       @SuppressWarnings("rawtypes")
578       Map<String, Hashtable> names = new LinkedHashMap<>();
579       for (int i = 0; i < sequences.length; i++)
580       {
581         SequenceI seq = sequences[i];
582         String newName = SeqsetUtils.unique_name(i);
583         @SuppressWarnings("rawtypes")
584         Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
585         names.put(newName, hash);
586         if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
587         {
588           String seqString = seq.getSequenceAsString();
589           if (!submitGaps)
590           {
591             seqString = AlignSeq.extractGaps(
592                 jalview.util.Comparison.GapChars, seqString);
593           }
594           inputSequences.add(new Sequence(newName, seqString));
595         }
596         else
597         {
598           String seqString = "";
599           if (seq.getEnd() >= seq.getStart()) // true if gaps only
600           {
601             seqString = seq.getSequenceAsString();
602             if (!submitGaps)
603             {
604               seqString = AlignSeq.extractGaps(
605                   jalview.util.Comparison.GapChars, seqString);
606             }
607           }
608           emptySequences.add(new Sequence(newName, seqString));
609         }
610       }
611
612       return new JobInput(numSeq, inputSequences, emptySequences, names);
613     }
614   }
615
616 }