JAL-3878 Move listeners list from the WebServiceDiscoverer interface.
[jalview.git] / src / jalview / ws2 / operations / AlignmentWorker.java
1 package jalview.ws2.operations;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.LinkedHashMap;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.Objects;
11 import java.util.function.Consumer;
12
13 import jalview.analysis.AlignSeq;
14 import jalview.analysis.AlignmentSorter;
15 import jalview.analysis.SeqsetUtils;
16 import jalview.analysis.SeqsetUtils.SequenceInfo;
17 import jalview.api.AlignViewportI;
18 import jalview.bin.Cache;
19 import jalview.datamodel.AlignedCodonFrame;
20 import jalview.datamodel.Alignment;
21 import jalview.datamodel.AlignmentI;
22 import jalview.datamodel.AlignmentOrder;
23 import jalview.datamodel.AlignmentView;
24 import jalview.datamodel.HiddenColumns;
25 import jalview.datamodel.Sequence;
26 import jalview.datamodel.SequenceI;
27 import jalview.gui.AlignViewport;
28 import jalview.util.MessageManager;
29 import jalview.ws.params.ArgumentI;
30 import jalview.ws2.WSJob;
31 import jalview.ws2.WSJobStatus;
32
33 public class AlignmentWorker extends AbstractPollableWorker
34 {
35   private AlignmentOperation operation;
36   
37   private Consumer<AlignmentResult> resultConsumer;
38   
39   private AlignmentView msa;
40
41   private AlignmentI dataset;
42
43   private AlignViewportI viewport;
44
45   private List<AlignedCodonFrame> codonFrame = new ArrayList<>();
46
47   private List<ArgumentI> args = Collections.emptyList();
48
49   private boolean submitGaps = false;
50
51   private boolean preserveOrder = false;
52
53   private char gapCharacter;
54
55   private WSJobList<AlignmentJob> jobs = new WSJobList<>();
56
57   private Map<Long, Integer> exceptionCount = new HashMap<>();
58
59   private static final int MAX_RETRY = 5;
60
61   private static class JobInput
62   {
63     final List<SequenceI> inputSequences;
64
65     final List<SequenceI> emptySequences;
66
67     final Map<String, SequenceInfo> sequenceNames;
68
69     private JobInput(List<SequenceI> inputSequences,
70         List<SequenceI> emptySequences,
71         Map<String, SequenceInfo> names)
72     {
73       this.inputSequences = Collections.unmodifiableList(inputSequences);
74       this.emptySequences = Collections.unmodifiableList(emptySequences);
75       this.sequenceNames = names;
76     }
77
78     boolean isInputValid()
79     {
80       return inputSequences.size() >= 2;
81     }
82   }
83   
84   public class AlignmentJob extends WSJob
85   {
86     private List<SequenceI> inputSequences;
87     private List<SequenceI> emptySequences;
88     private Map<String, SequenceInfo> sequenceNames;
89     
90     private AlignmentJob(String serviceProvider, String serviceName, 
91         String hostName)
92     {
93       super(serviceProvider, serviceName, hostName);
94     }
95     
96     private void setInput(JobInput input) {
97       inputSequences = input.inputSequences;
98       emptySequences = input.emptySequences;
99       sequenceNames = input.sequenceNames;
100     }
101   }
102
103   public class AlignmentResult
104   {
105     AlignmentI aln;
106
107     List<AlignmentOrder> alorders;
108
109     HiddenColumns hidden;
110
111     AlignmentResult(AlignmentI aln, List<AlignmentOrder> alorders,
112         HiddenColumns hidden)
113     {
114       this.aln = aln;
115       this.alorders = alorders;
116       this.hidden = hidden;
117     }
118
119     public AlignmentI getAlignment()
120     {
121       return aln;
122     }
123
124     public List<AlignmentOrder> getAlignmentOrders()
125     {
126       return alorders;
127     }
128
129     public HiddenColumns getHiddenColumns()
130     {
131       return hidden;
132     }
133   }
134
135   public AlignmentWorker(AlignmentOperation operation,
136       AlignmentView msa, List<ArgumentI> args,
137       boolean submitGaps, boolean preserveOrder, AlignViewportI viewport)
138   {
139     this.operation = operation;
140     this.msa = msa;
141     this.dataset = viewport.getAlignment().getDataset();
142     List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
143         viewport.getAlignment().getCodonFrames(), Collections.emptyList());
144     this.codonFrame.addAll(cf);
145     this.args = args;
146     this.submitGaps = submitGaps;
147     this.preserveOrder = preserveOrder;
148     this.viewport = viewport;
149     this.gapCharacter = viewport.getGapCharacter();
150   }
151
152   @Override
153   public Operation getOperation()
154   {
155     return operation;
156   }
157
158   @Override
159   public WSJobList<? extends WSJob> getJobs()
160   {
161     return jobs;
162   }
163   
164   public void setResultConsumer(Consumer<AlignmentResult> consumer)
165   {
166     this.resultConsumer = consumer;
167   }
168   
169   @Override
170   public void start() throws IOException
171   {
172     Cache.log.info(String.format("Starting new %s job.", operation.getName()));
173     SequenceI[][] conmsa = msa.getVisibleContigs('-');
174     if (conmsa == null)
175     {
176       return;
177     }
178     int numValid = 0;
179     for (int i =  0; i < conmsa.length; i++)
180     {
181       JobInput input = prepareInputData(conmsa[i], 2, submitGaps);
182       AlignmentJob job = new AlignmentJob(operation.service.getProviderName(),
183           operation.getName(), operation.getHostName());
184       job.setJobNum(i);
185       job.setInput(input);
186       jobs.add(job);
187       listeners.fireJobCreated(job);
188       if (input.isInputValid())
189       {
190         int count;
191         String jobId = null;
192         do
193         {
194           count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
195           try
196           {
197             jobId = operation.service.submit(input.inputSequences, args);
198             Cache.log.debug((String.format("Job %s submitted", job)));
199             exceptionCount.remove(job.getUid());
200           } catch (IOException e)
201           {
202             exceptionCount.put(job.getUid(), --count);
203           }
204         } while (jobId == null && count > 0);
205         if (jobId != null)
206         {
207           job.setJobId(jobId);
208           job.setStatus(WSJobStatus.SUBMITTED);
209           numValid++;
210         }
211         else
212         {
213           job.setStatus(WSJobStatus.SERVER_ERROR);
214         }
215       }
216       else
217       {
218         job.setStatus(WSJobStatus.INVALID);
219         job.setErrorLog(
220             MessageManager.getString("label.empty_alignment_job"));
221       }
222     }
223     if (numValid > 0)
224     {
225       listeners.fireWorkerStarted();
226     }
227     else
228     {
229       listeners.fireWorkerNotStarted();
230     }
231   }
232   
233
234   private static JobInput prepareInputData(SequenceI[] sequences,
235       int minLength, boolean submitGaps)
236   {
237     assert minLength >= 0 : MessageManager.getString(
238         "error.implementation_error_minlen_must_be_greater_zero");
239     int numSeq = 0;
240     for (SequenceI seq : sequences)
241     {
242       if (seq.getEnd() - seq.getStart() >= minLength)
243       {
244         numSeq++;
245       }
246     }
247
248     List<SequenceI> inputSequences = new ArrayList<>();
249     List<SequenceI> emptySequences = new ArrayList<>();
250     Map<String, SequenceInfo> names = new LinkedHashMap<>();
251     for (int i = 0; i < sequences.length; i++)
252     {
253       SequenceI seq = sequences[i];
254       String newName = SeqsetUtils.unique_name(i);
255       var hash = SeqsetUtils.SeqCharacterHash(seq);
256       names.put(newName, hash);
257       if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
258       {
259         String seqString = seq.getSequenceAsString();
260         if (!submitGaps)
261         {
262           seqString = AlignSeq.extractGaps(
263               jalview.util.Comparison.GapChars, seqString);
264         }
265         inputSequences.add(new Sequence(newName, seqString));
266       }
267       else
268       {
269         String seqString = "";
270         if (seq.getEnd() >= seq.getStart()) // true if gaps only
271         {
272           seqString = seq.getSequenceAsString();
273           if (!submitGaps)
274           {
275             seqString = AlignSeq.extractGaps(
276                 jalview.util.Comparison.GapChars, seqString);
277           }
278         }
279         emptySequences.add(new Sequence(newName, seqString));
280       }
281     }
282
283     return new JobInput(inputSequences, emptySequences, names);
284   }
285
286   @Override
287   public void done()
288   {
289     listeners.fireWorkerCompleting();
290     Map<Long, AlignmentI> results = new LinkedHashMap<>();
291     for (WSJob job : getJobs())
292     {
293       if (job.getStatus().isFailed())
294         continue;
295       try
296       {
297         AlignmentI alignment = operation.getAlignmentSupplier().getAlignment(job);
298         if (alignment != null)
299         {
300           results.put(job.getUid(), alignment);
301         }
302       } catch (Exception e)
303       {
304         if (!operation.getWebService().handleCollectionError(job, e))
305         {
306           Cache.log.error("Couldn't get alignment for job.", e);
307           // TODO: Increment exception count and retry.
308           job.setStatus(WSJobStatus.SERVER_ERROR);
309         }
310       }
311     }
312     if (results.size() > 0)
313     {
314       AlignmentResult out = prepareResult(results);
315       resultConsumer.accept(out);
316     }
317     else
318     {
319       resultConsumer.accept(null);
320     }
321     listeners.fireWorkerCompleted();
322   }
323
324   private AlignmentResult prepareResult(Map<Long, AlignmentI> alignments)
325   {
326     List<AlignmentOrder> alorders = new ArrayList<>();
327     SequenceI[][] results = new SequenceI[jobs.size()][];
328     AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
329     for (int i = 0; i < jobs.size(); i++)
330     {
331       AlignmentJob job = jobs.get(i);
332       AlignmentI aln = alignments.get(job.getUid());
333       if (aln != null) // equivalent of job.hasResults()
334       {
335         /* Get the alignment including any empty sequences in the original
336          * order with original ids. */
337         char gapChar = aln.getGapCharacter();
338         List<SequenceI> emptySeqs = job.emptySequences;
339         List<SequenceI> alnSeqs = aln.getSequences();
340         // find the width of the longest sequence
341         int width = 0;
342         for (var seq : alnSeqs)
343           width = Integer.max(width, seq.getLength());
344         for (var emptySeq : emptySeqs)
345           width = Integer.max(width, emptySeq.getLength());
346         // pad shorter sequences with gaps
347         String gapSeq = String.join("",
348             Collections.nCopies(width, Character.toString(gapChar)));
349         List<SequenceI> seqs = new ArrayList<>(
350             alnSeqs.size() + emptySeqs.size());
351         seqs.addAll(alnSeqs);
352         seqs.addAll(emptySeqs);
353         for (var seq : seqs)
354         {
355           if (seq.getLength() < width)
356             seq.setSequence(seq.getSequenceAsString()
357                 + gapSeq.substring(seq.getLength()));
358         }
359         SequenceI[] result = seqs.toArray(new SequenceI[0]);
360         AlignmentOrder msaOrder = new AlignmentOrder(result);
361         AlignmentSorter.recoverOrder(result);
362         Map<String, SequenceInfo> names = new HashMap<>(job.sequenceNames);
363         // FIXME first call to deuniquify alters original alignment
364         SeqsetUtils.deuniquify(names, result);
365         alorders.add(msaOrder);
366         results[i] = result;
367         orders[i] = msaOrder;
368       }
369       else
370       {
371         results[i] = null;
372       }
373     }
374
375     Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
376     // free references to original data
377     for (int i = 0; i < jobs.size(); i++)
378     {
379       results[i] = null;
380       orders[i] = null;
381     }
382     SequenceI[] alignment = (SequenceI[]) newView[0];
383     HiddenColumns hidden = (HiddenColumns) newView[1];
384     Alignment aln = new Alignment(alignment);
385     aln.setProperty("Alignment Program", operation.getName());
386     if (dataset != null)
387       aln.setDataset(dataset);
388
389     propagateDatasetMappings(aln);
390     return new AlignmentResult(aln, alorders, hidden);
391     // displayNewFrame(aln, alorders, hidden);
392   }
393
394   /*
395    * conserves dataset references to sequence objects returned from web
396    * services. propagate codon frame data to alignment.
397    */
398   private void propagateDatasetMappings(Alignment aln)
399   {
400     if (codonFrame != null)
401     {
402       SequenceI[] alignment = aln.getSequencesArray();
403       for (SequenceI seq : alignment)
404       {
405         for (AlignedCodonFrame acf : codonFrame)
406         {
407           if (acf != null && acf.involvesSequence(seq))
408           {
409             aln.addCodonFrame(acf);
410             break;
411           }
412         }
413       }
414     }
415   }
416 }