Merge branch 'mmw/JAL-4199-web-services-testing' into development/Release_2_12_Branch
[jalview.git] / src / jalview / ws2 / actions / alignment / AlignmentTask.java
1 package jalview.ws2.actions.alignment;
2
3 import static java.lang.String.format;
4
5 import java.io.IOException;
6 import java.util.ArrayList;
7 import java.util.Arrays;
8 import java.util.Collections;
9 import java.util.HashMap;
10 import java.util.List;
11 import java.util.Map;
12
13 import jalview.analysis.AlignmentSorter;
14 import jalview.analysis.SeqsetUtils;
15 import jalview.analysis.SeqsetUtils.SequenceInfo;
16 import jalview.api.AlignViewportI;
17 import jalview.bin.Cache;
18 import jalview.bin.Console;
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.ws.params.ArgumentI;
28 import jalview.ws2.actions.AbstractPollableTask;
29 import jalview.ws2.actions.ServiceInputInvalidException;
30 import jalview.ws2.actions.api.TaskEventListener;
31 import jalview.ws2.api.Credentials;
32 import jalview.ws2.api.JobStatus;
33 import jalview.ws2.client.api.AlignmentWebServiceClientI;
34
35 /**
36  * Implementation of an abstract pollable task used by alignment service
37  * actions.
38  * 
39  * @author mmwarowny
40  *
41  */
42 class AlignmentTask extends AbstractPollableTask<AlignmentJob, AlignmentResult>
43 {
44   /* task parameters set in the constructor */
45   private final AlignmentWebServiceClientI client;
46
47   private final AlignmentAction action;
48
49   private final AlignmentView msa; // a.k.a. input
50
51   private final AlignViewportI viewport;
52
53   private final boolean submitGaps;
54
55   private final AlignmentI currentView;
56
57   private final AlignmentI dataset;
58
59   private final char gapChar;
60
61   private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
62
63   AlignmentTask(AlignmentWebServiceClientI client, AlignmentAction action,
64       List<ArgumentI> args, Credentials credentials,
65       AlignmentView msa, AlignViewportI viewport, boolean submitGaps,
66       TaskEventListener<AlignmentResult> eventListener)
67   {
68     super(client, args, credentials, eventListener);
69     this.client = client;
70     this.action = action;
71     this.msa = msa;
72     this.viewport = viewport;
73     this.submitGaps = submitGaps;
74     this.currentView = viewport.getAlignment();
75     this.dataset = viewport.getAlignment().getDataset();
76     this.gapChar = viewport.getGapCharacter();
77     List<AlignedCodonFrame> cf = viewport.getAlignment().getCodonFrames();
78     if (cf != null)
79       this.codonFrame.addAll(cf);
80   }
81   
82   @Override
83   protected List<AlignmentJob> prepare() throws ServiceInputInvalidException
84   { 
85     Console.info(format("starting alignment service %s:%s",
86         client.getClientName(), action.getName()));
87     SequenceI[][] conmsa = msa.getVisibleContigs(gapChar);
88     if (conmsa == null)
89     {
90       throw new ServiceInputInvalidException("no visible contigs for alignment");
91     }
92     List<AlignmentJob> jobs = new ArrayList<>(conmsa.length);
93     boolean validInput = false;
94     for (int i = 0; i < conmsa.length; i++)
95     {
96       AlignmentJob job = AlignmentJob.create(conmsa[i], 2, submitGaps);
97       validInput |= job.isInputValid();  // at least one input is valid
98       job.setStatus(job.isInputValid() ? JobStatus.READY : JobStatus.INVALID);
99       jobs.add(job);
100     }
101     this.jobs = jobs;
102     if (!validInput)
103     {
104       throw new ServiceInputInvalidException("no valid sequences for alignment");
105     }
106     return jobs;
107   }
108
109   @Override
110   protected AlignmentResult done() throws IOException
111   {
112     IOException lastIOE = null;
113     for (AlignmentJob job : jobs)
114     {
115       if (job.isInputValid() && job.getStatus() == JobStatus.COMPLETED &&
116           !job.hasResult())
117       {
118         try
119         {
120           job.setAlignmentResult(client.getAlignment(job.getServerJob()));
121         } catch (IOException e)
122         {
123           lastIOE = e;
124         }
125       }
126     }
127     if (lastIOE != null)
128       throw lastIOE;  // do not proceed unless all results has been retrieved
129     
130     List<AlignmentOrder> alorders = new ArrayList<>();
131     SequenceI[][] results = new SequenceI[jobs.size()][];
132     AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
133     for (int i = 0; i < jobs.size(); i++)
134     {
135       /* alternative implementation of MsaWSJob#getAlignment */
136       AlignmentJob job = jobs.get(i);
137       if (!job.hasResult())
138         continue;
139       AlignmentI alignment = job.getAlignmentResult();
140       int alnSize = alignment.getSequences().size();
141       char gapChar = alnSize > 0 ? alignment.getGapCharacter() : '-';
142       List<SequenceI> emptySeqs = job.getEmptySequences();
143       List<SequenceI> alnSeqs = new ArrayList<>(alnSize);
144       // create copies of all sequences involved
145       for (SequenceI seq : alignment.getSequences())
146       {
147         alnSeqs.add(new Sequence(seq));
148       }
149       for (SequenceI seq : emptySeqs)
150       {
151         alnSeqs.add(new Sequence(seq));
152       }
153       // find the width of the longest sequence
154       int width = 0;
155       for (var seq: alnSeqs)
156         width = Integer.max(width, seq.getLength());
157       // make a sequence of gaps only to cut/paste
158       String gapSeq;
159       {
160         char[] gaps = new char[width];
161         Arrays.fill(gaps, gapChar);
162         gapSeq = new String(gaps);
163       }
164       for (var seq: alnSeqs)
165       {
166         if (seq.getLength() < width)
167         {
168           // pad sequences shorter than the target width with gaps
169           seq.setSequence(seq.getSequenceAsString()
170               + gapSeq.substring(seq.getLength()));
171         }
172       }
173       SequenceI[] result = alnSeqs.toArray(new SequenceI[0]);
174       AlignmentOrder msaOrder = new AlignmentOrder(result);
175       AlignmentSorter.recoverOrder(result);
176       Map<String, SequenceInfo> names = new HashMap<>(job.getNames());
177       SeqsetUtils.deuniquify(names, result);
178       
179       alorders.add(msaOrder);
180       results[i] = result;
181       orders[i] = msaOrder;
182     }
183     Object[] newView = msa.getUpdatedView(results, orders, gapChar);
184     // free references to original data
185     for (int i = 0; i < jobs.size(); i++)
186     {
187       results[i] = null;
188       orders[i] = null;
189     }
190     SequenceI[] alignment = (SequenceI[]) newView[0];
191     HiddenColumns hidden = (HiddenColumns) newView[1];
192     Alignment aln = new Alignment(alignment);
193     aln.setProperty("Alignment Program", action.getName());
194     if (dataset != null)
195       aln.setDataset(dataset);
196     
197     propagateDatasetMappings(aln);
198     return new AlignmentResult(aln, alorders, hidden);
199   }
200   
201   /**
202    * Conserve dataset references to sequence objects returned from web services.
203    * Propagate AlignedCodonFrame data from {@code codonFrame} to {@code aln}.
204    * TODO: Refactor to datamodel
205    */
206   private void propagateDatasetMappings(AlignmentI aln)
207   {
208     if (codonFrame != null)
209     {
210       SequenceI[] alignment = aln.getSequencesArray();
211       for (final SequenceI seq : alignment)
212       {
213         for (AlignedCodonFrame acf : codonFrame)
214         {
215           if (acf != null && acf.involvesSequence(seq))
216           {
217             aln.addCodonFrame(acf);
218             break;
219           }
220         }
221       }
222     }
223   }
224 }