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