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