Merge branch 'Jalview-JS/develop' into merge_js_develop
[jalview.git] / src / jalview / ws / rest / RestJobThread.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ws.rest;
22
23 import jalview.bin.Cache;
24 import jalview.datamodel.Alignment;
25 import jalview.datamodel.AlignmentAnnotation;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.AlignmentOrder;
28 import jalview.datamodel.Annotation;
29 import jalview.datamodel.HiddenColumns;
30 import jalview.datamodel.SequenceGroup;
31 import jalview.datamodel.SequenceI;
32 import jalview.gui.AlignFrame;
33 import jalview.gui.Desktop;
34 import jalview.gui.PaintRefresher;
35 import jalview.gui.WebserviceInfo;
36 import jalview.io.NewickFile;
37 import jalview.io.packed.JalviewDataset;
38 import jalview.io.packed.JalviewDataset.AlignmentSet;
39 import jalview.util.MessageManager;
40 import jalview.ws.AWSThread;
41 import jalview.ws.AWsJob;
42
43 import java.awt.event.ActionEvent;
44 import java.awt.event.ActionListener;
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.Hashtable;
48 import java.util.List;
49 import java.util.Map.Entry;
50
51 import org.apache.axis.transport.http.HTTPConstants;
52 import org.apache.http.Header;
53 import org.apache.http.HttpEntity;
54 import org.apache.http.HttpResponse;
55 import org.apache.http.client.ClientProtocolException;
56 import org.apache.http.client.methods.HttpGet;
57 import org.apache.http.client.methods.HttpPost;
58 import org.apache.http.client.methods.HttpRequestBase;
59 import org.apache.http.entity.mime.HttpMultipartMode;
60 import org.apache.http.entity.mime.MultipartEntity;
61 import org.apache.http.impl.client.DefaultHttpClient;
62 import org.apache.http.util.EntityUtils;
63
64 public class RestJobThread extends AWSThread
65 {
66   enum Stage
67   {
68     SUBMIT, POLL
69   }
70
71   protected RestClient restClient;
72
73   public RestJobThread(RestClient restClient)
74   {
75     super(restClient.af, null, restClient._input,
76             restClient.service.postUrl);
77     this.restClient = restClient; // may not be needed
78     // Test Code
79     // minimal job - submit given input and parse result onto alignment as
80     // annotation/whatever
81
82     // look for tree data, etc.
83
84     // for moment do following job type only
85     // input=visiblealignment,groupsindex
86     // ie one subjob using groups defined on alignment.
87     if (!restClient.service.isHseparable())
88     {
89       jobs = new RestJob[1];
90       jobs[0] = new RestJob(0, this,
91               restClient._input.getVisibleAlignment(
92                       restClient.service.getGapCharacter()),
93               restClient._input.getVisibleContigs());
94       // need a function to get a range on a view/alignment and return both
95       // annotation, groups and selection subsetted to just that region.
96
97     }
98     else
99     {
100       int[] viscontig = restClient._input.getVisibleContigs();
101       AlignmentI[] viscontigals = restClient._input
102               .getVisibleContigAlignments(
103                       restClient.service.getGapCharacter());
104       if (viscontigals != null && viscontigals.length > 0)
105       {
106         jobs = new RestJob[viscontigals.length];
107         for (int j = 0; j < jobs.length; j++)
108         {
109           int[] visc = new int[] { viscontig[j * 2], viscontig[j * 2 + 1] };
110           if (j != 0)
111           {
112             jobs[j] = new RestJob(j, this, viscontigals[j], visc);
113           }
114           else
115           {
116             jobs[j] = new RestJob(0, this, viscontigals[j], visc);
117           }
118         }
119       }
120     }
121     // end Test Code
122     /**
123      * subjob types row based: per sequence in alignment/selected region { input
124      * is one sequence or sequence ID } per alignment/selected region { input is
125      * set of sequences, alignment, one or more sets of sequence IDs,
126      */
127
128     if (!restClient.service.isHseparable())
129     {
130
131       // create a bunch of subjobs per visible contig to ensure result honours
132       // hidden boundaries
133       // TODO: determine if a 'soft' hSeperable property is needed - e.g. if
134       // user does SS-pred on sequence with big hidden regions, its going to be
135       // less reliable.
136     }
137     else
138     {
139       // create a single subjob for the visible/selected input
140
141     }
142     // TODO: decide if vSeperable exists: eg. column wide analysis where hidden
143     // rows do not affect output - generally no analysis that generates
144     // alignment annotation is vSeparable -
145   }
146
147   /**
148    * create gui components for monitoring jobs
149    * 
150    * @param webserviceInfo
151    */
152   public void setWebServiceInfo(WebserviceInfo webserviceInfo)
153   {
154     wsInfo = webserviceInfo;
155     for (int j = 0; j < jobs.length; j++)
156     {
157       wsInfo.addJobPane();
158       // Copy over any per job params
159       if (jobs.length > 1)
160       {
161         wsInfo.setProgressName("region " + jobs[j].getJobnum(),
162                 jobs[j].getJobnum());
163       }
164       else
165       {
166         wsInfo.setProgressText(jobs[j].getJobnum(), OutputHeader);
167       }
168     }
169   }
170
171   private String getStage(Stage stg)
172   {
173     if (stg == Stage.SUBMIT)
174     {
175       return "submitting ";
176     }
177     if (stg == Stage.POLL)
178     {
179       return "checking status of ";
180     }
181
182     return (" being confused about ");
183   }
184
185   private void doPoll(RestJob rj) throws Exception
186   {
187     String postUrl = rj.getPollUrl();
188     doHttpReq(Stage.POLL, rj, postUrl);
189   }
190
191   /**
192    * construct the post and handle the response.
193    * 
194    * @throws Exception
195    */
196   public void doPost(RestJob rj) throws Exception
197   {
198     String postUrl = rj.getPostUrl();
199     doHttpReq(Stage.SUBMIT, rj, postUrl);
200     wsInfo.invalidate();
201   }
202
203   /**
204    * do the HTTP request - and respond/set statuses appropriate to the current
205    * stage.
206    * 
207    * @param stg
208    * @param rj
209    *          - provides any data needed for posting and used to record state
210    * @param postUrl
211    *          - actual URL to post/get from
212    * @throws Exception
213    */
214   protected void doHttpReq(Stage stg, RestJob rj, String postUrl)
215           throws Exception
216   {
217     HttpRequestBase request = null;
218     String messages = "";
219     if (stg == Stage.SUBMIT)
220     {
221       // Got this from
222       // http://evgenyg.wordpress.com/2010/05/01/uploading-files-multipart-post-apache/
223
224       HttpPost htpost = new HttpPost(postUrl);
225       MultipartEntity postentity = new MultipartEntity(
226               HttpMultipartMode.STRICT);
227       for (Entry<String, InputType> input : rj.getInputParams())
228       {
229         if (input.getValue().validFor(rj))
230         {
231           postentity.addPart(input.getKey(),
232                   input.getValue().formatForInput(rj));
233         }
234         else
235         {
236           messages += "Skipped an input (" + input.getKey()
237                   + ") - Couldn't generate it from available data.";
238         }
239       }
240       htpost.setEntity(postentity);
241       request = htpost;
242     }
243     else
244     {
245       request = new HttpGet(postUrl);
246     }
247     if (request != null)
248     {
249       DefaultHttpClient httpclient = new DefaultHttpClient();
250
251       HttpResponse response = null;
252       try
253       {
254         response = httpclient.execute(request);
255       } catch (ClientProtocolException he)
256       {
257         rj.statMessage = "Web Protocol Exception when " + getStage(stg)
258                 + "Job. <br>Problematic url was <a href=\""
259                 + request.getURI() + "\">" + request.getURI()
260                 + "</a><br>See Console output for details.";
261         rj.setAllowedServerExceptions(0);// unrecoverable;
262         rj.error = true;
263         Cache.log.fatal("Unexpected REST Job " + getStage(stg)
264                 + "exception for URL " + rj.rsd.postUrl);
265         throw (he);
266       } catch (IOException e)
267       {
268         rj.statMessage = "IO Exception when " + getStage(stg)
269                 + "Job. <br>Problematic url was <a href=\""
270                 + request.getURI() + "\">" + request.getURI()
271                 + "</a><br>See Console output for details.";
272         Cache.log.warn("IO Exception for REST Job " + getStage(stg)
273                 + "exception for URL " + rj.rsd.postUrl);
274
275         throw (e);
276       }
277       switch (response.getStatusLine().getStatusCode())
278       {
279       case 200:
280         rj.running = false;
281         Cache.log.debug("Processing result set.");
282         processResultSet(rj, response, request);
283         break;
284
285       case 202:
286         markJobAsRunning(rj);
287         break;
288
289       case 201:
290         // Created - redirect may be present. Fallthrough to 302
291       case 302:
292         extractJobId(rj, response);
293         completeStatus(rj, response);
294         break;
295       case 500:
296         markAsFailed(rj, response);
297         completeStatus(rj, response, "" + getStage(stg)
298                 + "failed. Reason below:\n");
299         break;
300       default:
301         // Some other response. Probably need to pop up the content in a window.
302         // TODO: deal with all other HTTP response codes from server.
303         Cache.log.warn("Unhandled response status when " + getStage(stg)
304                 + "for " + postUrl + ": " + response.getStatusLine());
305         rj.error = true;
306         rj.setAllowedServerExceptions(0);
307         rj.setSubjobComplete(true);
308         rj.setSubmitted(true);
309         try
310         {
311           completeStatus(rj, response, "" + getStage(stg)
312                   + " resulted in an unexpected server response.<br/>Url concerned was <a href=\""
313                   + request.getURI() + "\">" + request.getURI()
314                   + "</a><br/>Filtered response content below:<br/>");
315         } catch (IOException e)
316         {
317           Cache.log.debug("IOException when consuming unhandled response",
318                   e);
319         }
320         ;
321       }
322     }
323   }
324
325   private void markAsFailed(RestJob rj, HttpResponse response)
326   {
327     // Failed.
328     rj.setSubmitted(true);
329     rj.setAllowedServerExceptions(0);
330     rj.setSubjobComplete(true);
331     rj.error = true;
332     rj.running = false;
333   }
334
335   /**
336    * set the jobRunning flag and post a link to the physical result page encoded
337    * in rj.getJobId()
338    * 
339    * @param rj
340    */
341   private void markJobAsRunning(RestJob rj)
342   {
343     rj.statMessage = "<br>Job submitted successfully. Results available at this URL:\n"
344             + "<a href="
345             + rj.getJobId()
346             + "\">"
347             + rj.getJobId()
348             + "</a><br>";
349     rj.running = true;
350   }
351
352   /**
353    * extract the job ID URL from the redirect page. Does nothing if job is
354    * already running.
355    * 
356    * @param rj
357    * @param response
358    */
359   private void extractJobId(RestJob rj, HttpResponse response)
360   {
361     Header[] loc;
362     if (!rj.isSubmitted())
363     {
364
365       // redirect URL - typical for IBIVU type jobs.
366       if ((loc = response.getHeaders(HTTPConstants.HEADER_LOCATION)) != null
367               && loc.length > 0)
368       {
369         if (loc.length > 1)
370         {
371           Cache.log
372                   .warn("Ignoring additional "
373                           + (loc.length - 1)
374                           + " location(s) provided in response header ( next one is '"
375                           + loc[1].getValue() + "' )");
376         }
377         rj.setJobId(loc[0].getValue());
378         rj.setSubmitted(true);
379       }
380     }
381   }
382
383   /**
384    * job has completed. Something valid should be available from con
385    * 
386    * @param rj
387    * @param con
388    * @param req
389    *          is a stateless request - expected to return the same data
390    *          regardless of how many times its called.
391    */
392   private void processResultSet(RestJob rj, HttpResponse con,
393           HttpRequestBase req)
394   {
395     if (rj.statMessage == null)
396     {
397       rj.statMessage = "";
398     }
399     rj.statMessage += "Job Complete.\n";
400     try
401     {
402       rj.resSet = new HttpResultSet(rj, con, req);
403       rj.gotresult = true;
404     } catch (IOException e)
405     {
406       rj.statMessage += "Couldn't parse results. Failed.";
407       rj.error = true;
408       rj.gotresult = false;
409     }
410   }
411
412   private void completeStatus(RestJob rj, HttpResponse con)
413           throws IOException
414   {
415     completeStatus(rj, con, null);
416
417   }
418
419   private void completeStatus(RestJob rj, HttpResponse con, String prefix)
420           throws IOException
421   {
422     StringBuffer sb = new StringBuffer();
423     if (prefix != null)
424     {
425       sb.append(prefix);
426     }
427     ;
428     if (rj.statMessage != null && rj.statMessage.length() > 0)
429     {
430       sb.append(rj.statMessage);
431     }
432     HttpEntity en = con.getEntity();
433     /*
434      * Just append the content as a string.
435      */
436     String f;
437     StringBuffer content = new StringBuffer(f = EntityUtils.toString(en));
438     f = f.toLowerCase();
439     int body = f.indexOf("<body");
440     if (body > -1)
441     {
442       content.delete(0, f.indexOf(">", body) + 1);
443     }
444     if (body > -1 && sb.length() > 0)
445     {
446       sb.append("\n");
447       content.insert(0, sb);
448       sb = null;
449     }
450     f = null;
451     rj.statMessage = content.toString();
452   }
453
454   @Override
455   public void pollJob(AWsJob job) throws Exception
456   {
457     assert (job instanceof RestJob);
458     System.err.println("Debug RestJob: Polling Job");
459     doPoll((RestJob) job);
460   }
461
462   @Override
463   public void StartJob(AWsJob job)
464   {
465     assert (job instanceof RestJob);
466     try
467     {
468       System.err.println("Debug RestJob: Posting Job");
469       doPost((RestJob) job);
470     } catch (NoValidInputDataException erex)
471     {
472       job.setSubjobComplete(true);
473       job.setSubmitted(true);
474       ((RestJob) job).statMessage = "<br>It looks like there was a problem with the data sent to the service :<br>"
475               + erex.getMessage() + "\n";
476       ((RestJob) job).error = true;
477
478     } catch (Exception ex)
479     {
480       job.setSubjobComplete(true);
481       job.setAllowedServerExceptions(-1);
482       Cache.log.error("Exception when trying to start Rest Job.", ex);
483     }
484   }
485
486   @Override
487   public void parseResult()
488   {
489     // crazy users will see this message
490     // TODO: finish this! and remove the message below!
491     Cache.log.warn("Rest job result parser is currently INCOMPLETE!");
492     int validres = 0;
493     for (RestJob rj : (RestJob[]) jobs)
494     {
495       if (rj.hasResponse() && rj.resSet != null && rj.resSet.isValid())
496       {
497         String ln = null;
498         try
499         {
500           Cache.log.debug("Parsing data for job " + rj.getJobId());
501           rj.parseResultSet();
502           if (rj.hasResults())
503           {
504             validres++;
505           }
506           Cache.log.debug("Finished parsing data for job " + rj.getJobId());
507
508         } catch (Error ex)
509         {
510           Cache.log.warn(
511                   "Failed to finish parsing data for job " + rj.getJobId());
512           ex.printStackTrace();
513         } catch (Exception ex)
514         {
515           Cache.log.warn(
516                   "Failed to finish parsing data for job " + rj.getJobId());
517           ex.printStackTrace();
518         } finally
519         {
520           rj.error = true;
521           rj.statMessage = "Error whilst parsing data for this job.<br>URL for job response is :<a href=\""
522                   + rj.resSet.getUrl() + "\">" + rj.resSet.getUrl()
523                   + "</a><br>";
524         }
525       }
526     }
527     if (validres > 0)
528     {
529       // add listeners and activate result display gui elements
530       /**
531        * decisions based on job result content + state of alignFrame that
532        * originated the job:
533        */
534       /*
535        * 1. Can/Should this job automatically open a new window for results
536        */
537       if (true)
538       {
539         // preserver current jalview behaviour
540         wsInfo.setViewResultsImmediatly(true);
541       }
542       else
543       {
544         // realiseResults(true, true);
545       }
546       // otherwise, should automatically view results
547
548       // TODO: check if at least one or more contexts are valid - if so, enable
549       // gui
550       wsInfo.showResultsNewFrame.addActionListener(new ActionListener()
551       {
552
553         @Override
554         public void actionPerformed(ActionEvent e)
555         {
556           realiseResults(false);
557         }
558
559       });
560       wsInfo.mergeResults.addActionListener(new ActionListener()
561       {
562
563         @Override
564         public void actionPerformed(ActionEvent e)
565         {
566           realiseResults(true);
567         }
568
569       });
570
571       wsInfo.setResultsReady();
572     }
573     else
574     {
575       // tell the user nothing was returned.
576       wsInfo.setStatus(wsInfo.STATE_STOPPED_ERROR);
577       wsInfo.setFinishedNoResults();
578     }
579   }
580
581   /**
582    * instructions for whether to create new alignment view on current alignment
583    * set, add to current set, or create new alignFrame
584    */
585   private enum AddDataTo
586   {
587     /**
588      * add annotation, trees etc to current view
589      */
590     currentView,
591     /**
592      * create a new view derived from current view and add data to that
593      */
594     newView,
595     /**
596      * create a new alignment frame for the result set and add annotation to
597      * that.
598      */
599     newAlignment
600   };
601
602   public void realiseResults(boolean merge)
603   {
604     /*
605      * 2. Should the job modify the parent alignment frame/view(s) (if they
606      * still exist and the alignment hasn't been edited) in order to display new
607      * annotation/features.
608      */
609     /**
610      * alignment panels derived from each alignment set returned by service.
611      */
612     ArrayList<jalview.gui.AlignmentPanel> destPanels = new ArrayList<>();
613     /**
614      * list of instructions for how to process each distinct alignment set
615      * returned by the job set
616      */
617     ArrayList<AddDataTo> resultDest = new ArrayList<>();
618     /**
619      * when false, zeroth pane is panel derived from input deta.
620      */
621     boolean newAlignment = false;
622     /**
623      * gap character to be used for alignment reconstruction
624      */
625     char gapCharacter = restClient.av.getGapCharacter();
626     // Now, iterate over all alignment sets returned from all jobs:
627     // first inspect jobs and collate results data in order to count alignments
628     // and other results
629     // then assemble results from decomposed (v followed by h-separated) jobs
630     // finally, new views and alignments will be created and displayed as
631     // necessary.
632     boolean hsepjobs = restClient.service.isHseparable();
633     boolean vsepjobs = restClient.service.isVseparable();
634     // total number of distinct alignment sets generated by job set.
635     int numAlSets = 0, als = 0;
636     List<AlignmentI> destAls = new ArrayList<>();
637     List<jalview.datamodel.HiddenColumns> destColsel = new ArrayList<>();
638     List<List<NewickFile>> trees = new ArrayList<>();
639
640     do
641     {
642       // Step 1.
643       // iterate over each alignment set returned from each subjob. Treating
644       // each one returned in parallel with others.
645       // Result collation arrays
646
647       /**
648        * mapping between index of sequence in alignment that was submitted to
649        * server and index of sequence in the input alignment
650        */
651       int[][] ordermap = new int[jobs.length][];
652       SequenceI[][] rseqs = new SequenceI[jobs.length][];
653       AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
654       AlignmentAnnotation[][] alan = new AlignmentAnnotation[jobs.length][];
655       SequenceGroup[][] sgrp = new SequenceGroup[jobs.length][];
656       // Now collect all data for alignment Set als from job array
657       for (int j = 0; j < jobs.length; j++)
658       {
659         RestJob rj = (RestJob) jobs[j];
660         if (rj.hasResults())
661         {
662           JalviewDataset rset = rj.context;
663           if (rset.hasAlignments())
664           {
665             if (numAlSets < rset.getAl().size())
666             {
667               numAlSets = rset.getAl().size();
668             }
669             if (als < rset.getAl().size()
670                     && rset.getAl().get(als).isModified())
671             {
672               // Collate result data
673               // TODO: decide if all alignmentI should be collected rather than
674               // specific alignment data containers
675               // for moment, we just extract content, but this means any
676               // alignment properties may be lost.
677               AlignmentSet alset = rset.getAl().get(als);
678               alan[j] = alset.al.getAlignmentAnnotation();
679               if (alset.al.getGroups() != null)
680               {
681                 sgrp[j] = new SequenceGroup[alset.al.getGroups().size()];
682                 alset.al.getGroups().toArray(sgrp[j]);
683               }
684               else
685               {
686                 sgrp[j] = null;
687               }
688               orders[j] = new AlignmentOrder(alset.al);
689               rseqs[j] = alset.al.getSequencesArray();
690               ordermap[j] = rj.getOrderMap();
691               // if (rj.isInputUniquified()) {
692               // jalview.analysis.AlignmentSorter.recoverOrder(rseqs[als]);
693               // }
694
695               if (alset.trees != null)
696               {
697                 trees.add(new ArrayList<>(alset.trees));
698               }
699               else
700               {
701                 trees.add(new ArrayList<NewickFile>());
702               }
703             }
704           }
705         }
706       }
707       // Now aggregate and present results from this frame of alignment data.
708       int nvertsep = 0, nvertseps = 1;
709       if (vsepjobs)
710       {
711         // Jobs relate to different rows of input alignment.
712         // Jobs are subdivided by rows before columns,
713         // so there will now be a number of subjobs according tohsep for each
714         // vertsep
715         // TODO: get vertical separation intervals for each job and set
716         // nvertseps
717         // TODO: merge data from each group/sequence onto whole
718         // alignment
719       }
720       /**
721        * index into rest jobs subdivided vertically
722        */
723       int vrestjob = 0;
724       // Destination alignments for all result data.
725       ArrayList<SequenceGroup> visgrps = new ArrayList<>();
726       Hashtable<String, SequenceGroup> groupNames = new Hashtable<>();
727       ArrayList<AlignmentAnnotation> visAlAn = null;
728       for (nvertsep = 0; nvertsep < nvertseps; nvertsep++)
729       {
730         // TODO: set scope w.r.t. original alignment view for vertical
731         // separation.
732         {
733           // results for a job exclude hidden columns of input data, so map
734           // back on to all visible regions
735           /**
736            * rest job result we are working with
737            */
738           int nrj = vrestjob;
739
740           RestJob rj = (RestJob) jobs[nrj];
741           int contigs[] = input.getVisibleContigs();
742           AlignmentI destAl = null;
743           jalview.datamodel.HiddenColumns destHCs = null;
744           // Resolve destAl for this data.
745           if (als == 0 && rj.isInputContextModified())
746           {
747             // special case: transfer features, annotation, groups, etc,
748             // from input
749             // context to align panel derived from input data
750             if (destAls.size() > als)
751             {
752               destAl = destAls.get(als);
753             }
754             else
755             {
756               if (!restClient.isAlignmentModified() && merge)
757               {
758                 destAl = restClient.av.getAlignment();
759                 destHCs = restClient.av.getAlignment().getHiddenColumns();
760                 resultDest.add(restClient.isShowResultsInNewView()
761                         ? AddDataTo.newView
762                         : AddDataTo.currentView);
763                 destPanels.add(restClient.recoverAlignPanelForView());
764               }
765               else
766               {
767                 newAlignment = true;
768                 // recreate the input alignment data
769                 Object[] idat = input
770                         .getAlignmentAndHiddenColumns(gapCharacter);
771                 destAl = new Alignment((SequenceI[]) idat[0]);
772                 destHCs = (HiddenColumns) idat[1];
773                 resultDest.add(AddDataTo.newAlignment);
774                 // but do not add to the alignment panel list - since we need to
775                 // create a whole new alignFrame set.
776               }
777               destAls.add(destAl);
778               destColsel.add(destHCs);
779             }
780           }
781           else
782           {
783             // alignment(s) returned by service is to be re-integrated and
784             // displayed
785             if (destAls.size() > als)
786             {
787               if (!vsepjobs)
788               {
789                 // TODO: decide if multiple multiple alignments returned by
790                 // non-vseparable services are allowed.
791                 Cache.log.warn(
792                         "dealing with multiple alignment products returned by non-vertically separable service.");
793               }
794               // recover reference to last alignment created for this rest frame
795               // ready for extension
796               destAl = destAls.get(als);
797               destHCs = destColsel.get(als);
798             }
799             else
800             {
801               Object[] newview;
802
803               if (!hsepjobs)
804               {
805                 // single alignment for any job that gets mapped back on to
806                 // input data. Reconstruct by interleaving parts of returned
807                 // alignment with hidden parts of input data.
808                 SequenceI[][] nsq = splitSeqsOnVisibleContigs(rseqs[nrj],
809                         contigs, gapCharacter);
810                 AlignmentOrder alo[] = new AlignmentOrder[nsq.length];
811                 for (int no = 0; no < alo.length; no++)
812                 {
813                   alo[no] = new AlignmentOrder(orders[nrj].getOrder());
814                 }
815                 newview = input.getUpdatedView(nsq, orders, gapCharacter);
816               }
817               else
818               {
819                 // each job maps to a single visible contig, and all need to be
820                 // stitched back together.
821                 // reconstruct using sub-region based MSA alignment construction
822                 // mechanism
823                 newview = input.getUpdatedView(rseqs, orders, gapCharacter);
824               }
825               destAl = new Alignment((SequenceI[]) newview[0]);
826               destHCs = (HiddenColumns) newview[1];
827               newAlignment = true;
828               // TODO create alignment from result data with propagated
829               // references.
830               destAls.add(destAl);
831               destColsel.add(destHCs);
832               resultDest.add(AddDataTo.newAlignment);
833               throw new Error(
834                       MessageManager.getString("error.implementation_error")
835                               + "TODO: ");
836             }
837           }
838           /**
839            * save initial job in this set in case alignment is h-separable
840            */
841           int initnrj = nrj;
842           // Now add in groups
843           for (int ncnt = 0; ncnt < contigs.length; ncnt += 2)
844           {
845             if (!hsepjobs)
846             {
847               // single alignment for any job that gets mapped back on to input
848               // data.
849             }
850             else
851             {
852               // each job maps to a single visible contig, and all need to be
853               // stitched back together.
854               if (ncnt > 0)
855               {
856                 nrj++;
857               }
858               // TODO: apply options for group merging and annotation merging.
859               // If merging not supported, then either clear hashtables now or
860               // use them to rename the new annotation/groups for each contig if
861               // a conflict occurs.
862             }
863             if (sgrp[nrj] != null)
864             {
865               for (SequenceGroup sg : sgrp[nrj])
866               {
867                 boolean recovered = false;
868                 SequenceGroup exsg = null;
869                 if (sg.getName() != null)
870                 {
871                   exsg = groupNames.get(sg.getName());
872                 }
873                 if (exsg == null)
874                 {
875                   exsg = new SequenceGroup(sg);
876                   groupNames.put(exsg.getName(), exsg);
877                   visgrps.add(exsg);
878                   exsg.setStartRes(sg.getStartRes() + contigs[ncnt]);
879                   exsg.setEndRes(sg.getEndRes() + contigs[ncnt]);
880                 }
881                 else
882                 {
883                   recovered = true;
884                 }
885                 // now replace any references from the result set with
886                 // corresponding refs from alignment input set.
887
888                 // TODO: cope with recovering hidden sequences from
889                 // resultContext
890                 {
891                   for (SequenceI oseq : sg.getSequences(null))
892                   {
893                     SequenceI nseq = getNewSeq(oseq, rseqs[nrj],
894                             ordermap[nrj], destAl);
895                     if (nseq != null)
896                     {
897                       if (!recovered)
898                       {
899                         exsg.deleteSequence(oseq, false);
900                       }
901                       exsg.addSequence(nseq, false);
902                     }
903                     else
904                     {
905                       Cache.log.warn(
906                               "Couldn't resolve original sequence for new sequence.");
907                     }
908                   }
909                   if (sg.hasSeqrep())
910                   {
911                     if (exsg.getSeqrep() == sg.getSeqrep())
912                     {
913                       // lift over sequence rep reference too
914                       SequenceI oseq = sg.getSeqrep();
915                       SequenceI nseq = getNewSeq(oseq, rseqs[nrj],
916                               ordermap[nrj], destAl);
917                       if (nseq != null)
918                       {
919                         exsg.setSeqrep(nseq);
920                       }
921                     }
922                   }
923                 }
924                 if (recovered)
925                 {
926                   // adjust boundaries of recovered group w.r.t. new group being
927                   // merged on to original alignment.
928                   int start = sg.getStartRes() + contigs[ncnt],
929                           end = sg.getEndRes() + contigs[ncnt];
930                   if (start < exsg.getStartRes())
931                   {
932                     exsg.setStartRes(start);
933                   }
934                   if (end > exsg.getEndRes())
935                   {
936                     exsg.setEndRes(end);
937                   }
938                 }
939               }
940             }
941           }
942           // reset job counter
943           nrj = initnrj;
944           // and finally add in annotation and any trees for each job
945           for (int ncnt = 0; ncnt < contigs.length; ncnt += 2)
946           {
947             if (!hsepjobs)
948             {
949               // single alignment for any job that gets mapped back on to input
950               // data.
951             }
952             else
953             {
954               // each job maps to a single visible contig, and all need to be
955               // stitched back together.
956               if (ncnt > 0)
957               {
958                 nrj++;
959               }
960             }
961
962             // merge alignmentAnnotation into one row
963             if (alan[nrj] != null)
964             {
965               for (int an = 0; an < alan[nrj].length; an++)
966               {
967                 SequenceI sqass = null;
968                 SequenceGroup grass = null;
969                 if (alan[nrj][an].sequenceRef != null)
970                 {
971                   // TODO: ensure this relocates sequence reference to local
972                   // context.
973                   sqass = getNewSeq(alan[nrj][an].sequenceRef, rseqs[nrj],
974                           ordermap[nrj], destAl);
975                 }
976                 if (alan[nrj][an].groupRef != null)
977                 {
978                   // TODO: verify relocate group reference to local context
979                   // works correctly
980                   grass = groupNames.get(alan[nrj][an].groupRef.getName());
981                   if (grass == null)
982                   {
983                     Cache.log.error(
984                             "Couldn't relocate group referemce for group "
985                                     + alan[nrj][an].groupRef.getName());
986                   }
987                 }
988                 if (visAlAn == null)
989                 {
990                   visAlAn = new ArrayList<>();
991                 }
992                 AlignmentAnnotation visan = null;
993                 for (AlignmentAnnotation v : visAlAn)
994                 {
995                   if (v.label != null
996                           && v.label.equals(alan[nrj][an].label))
997                   {
998                     visan = v;
999                   }
1000                 }
1001                 if (visan == null)
1002                 {
1003                   visan = new AlignmentAnnotation(alan[nrj][an]);
1004                   // copy annotations, and wipe out/update refs.
1005                   visan.annotations = new Annotation[input.getWidth()];
1006                   visan.groupRef = grass;
1007                   visan.sequenceRef = sqass;
1008                   visAlAn.add(visan);
1009                 }
1010                 if (contigs[ncnt]
1011                         + alan[nrj][an].annotations.length > visan.annotations.length)
1012                 {
1013                   // increase width of annotation row
1014                   Annotation[] newannv = new Annotation[contigs[ncnt]
1015                           + alan[nrj][an].annotations.length];
1016                   System.arraycopy(visan.annotations, 0, newannv, 0,
1017                           visan.annotations.length);
1018                   visan.annotations = newannv;
1019                 }
1020                 // now copy local annotation data into correct position
1021                 System.arraycopy(alan[nrj][an].annotations, 0,
1022                         visan.annotations, contigs[ncnt],
1023                         alan[nrj][an].annotations.length);
1024
1025               }
1026             }
1027             // Trees
1028             if (trees.get(nrj).size() > 0)
1029             {
1030               for (NewickFile nf : trees.get(nrj))
1031               {
1032                 // TODO: process each newick file, lifting over sequence refs to
1033                 // current alignment, if necessary.
1034                 Cache.log.error(
1035                         "Tree recovery from restjob not yet implemented.");
1036               }
1037             }
1038           }
1039         }
1040       } // end of vseps loops.
1041       if (visAlAn != null)
1042       {
1043         for (AlignmentAnnotation v : visAlAn)
1044         {
1045           destAls.get(als).addAnnotation(v);
1046         }
1047       }
1048       if (visgrps != null)
1049       {
1050         for (SequenceGroup sg : visgrps)
1051         {
1052           destAls.get(als).addGroup(sg);
1053         }
1054       }
1055     } while (++als < numAlSets);
1056     // Finally, assemble each new alignment, and create new gui components to
1057     // present it.
1058     /**
1059      * current AlignFrame where results will go.
1060      */
1061     AlignFrame destaf = restClient.recoverAlignFrameForView();
1062     /**
1063      * current pane being worked with
1064      */
1065     jalview.gui.AlignmentPanel destPanel = restClient
1066             .recoverAlignPanelForView();
1067     als = 0;
1068     for (AddDataTo action : resultDest)
1069     {
1070       AlignmentI destal;
1071       HiddenColumns destcs;
1072       String alTitle = MessageManager
1073               .formatMessage("label.webservice_job_title_on", new String[]
1074               { restClient.service.details.getAction(),
1075                   restClient.service.details.getName(),
1076                   restClient.viewTitle });
1077       switch (action)
1078       {
1079       case newAlignment:
1080         destal = destAls.get(als);
1081         destcs = destColsel.get(als);
1082         destaf = new AlignFrame(destal, destcs, AlignFrame.DEFAULT_WIDTH,
1083                 AlignFrame.DEFAULT_HEIGHT);
1084         PaintRefresher.Refresh(destaf,
1085                 destaf.getViewport().getSequenceSetId());
1086         // todo transfer any feature settings and colouring
1087         /*
1088          * destaf.getFeatureRenderer().transferSettings(this.featureSettings);
1089          * // update orders if (alorders.size() > 0) { if (alorders.size() == 1)
1090          * { af.addSortByOrderMenuItem(WebServiceName + " Ordering",
1091          * (AlignmentOrder) alorders.get(0)); } else { // construct a
1092          * non-redundant ordering set Vector names = new Vector(); for (int i =
1093          * 0, l = alorders.size(); i < l; i++) { String orderName = new
1094          * String(" Region " + i); int j = i + 1;
1095          * 
1096          * while (j < l) { if (((AlignmentOrder) alorders.get(i))
1097          * .equals(((AlignmentOrder) alorders.get(j)))) { alorders.remove(j);
1098          * l--; orderName += "," + j; } else { j++; } }
1099          * 
1100          * if (i == 0 && j == 1) { names.add(new String("")); } else {
1101          * names.add(orderName); } } for (int i = 0, l = alorders.size(); i < l;
1102          * i++) { af.addSortByOrderMenuItem( WebServiceName + ((String)
1103          * names.get(i)) + " Ordering", (AlignmentOrder) alorders.get(i)); } } }
1104          */
1105         // TODO: modify this and previous alignment's title if many alignments
1106         // have been returned.
1107         Desktop.addInternalFrame(destaf, alTitle, AlignFrame.DEFAULT_WIDTH,
1108                 AlignFrame.DEFAULT_HEIGHT);
1109
1110         break;
1111       case newView:
1112         // TODO: determine title for view
1113         break;
1114       case currentView:
1115         break;
1116       }
1117     }
1118     if (!newAlignment)
1119     {
1120       if (restClient.isShowResultsInNewView())
1121       {
1122         // destPanel = destPanel.alignFrame.newView(false);
1123       }
1124     }
1125     else
1126     {
1127
1128     }
1129     /*
1130      * if (als) // add the destination panel to frame zero of result panel set }
1131      * } if (destPanels.size()==0) { AlignFrame af = new AlignFrame((AlignmentI)
1132      * idat[0], (ColumnSelection) idat[1], AlignFrame.DEFAULT_WIDTH,
1133      * AlignFrame.DEFAULT_HEIGHT);
1134      * 
1135      * jalview.gui.Desktop.addInternalFrame(af, "Results for " +
1136      * restClient.service.details.Name + " " + restClient.service.details.Action
1137      * + " on " + restClient.af.getTitle(), AlignFrame.DEFAULT_WIDTH,
1138      * AlignFrame.DEFAULT_HEIGHT); destPanel = af.alignPanel; // create totally
1139      * new alignment from stashed data/results
1140      */
1141
1142     /*
1143      */
1144
1145     /**
1146      * alignments. New alignments are added to dataset, and subsequently
1147      * annotated/visualised accordingly. 1. New alignment frame created for new
1148      * alignment. Decide if any vis settings should be inherited from old
1149      * alignment frame (e.g. sequence ordering ?). 2. Subsequent data added to
1150      * alignment as below:
1151      */
1152     /**
1153      * annotation update to original/newly created context alignment: 1.
1154      * identify alignment where annotation is to be loaded onto. 2. Add
1155      * annotation, excluding any duplicates. 3. Ensure annotation is visible on
1156      * alignment - honouring ordering given by file.
1157      */
1158     /**
1159      * features updated to original or newly created context alignment: 1.
1160      * Features are(or were already) added to dataset. 2. Feature settings
1161      * modified to ensure all features are displayed - honouring any ordering
1162      * given by result file. Consider merging action with the code used by the
1163      * DAS fetcher to update alignment views with new info.
1164      */
1165     /**
1166      * Seq associated data files (PDB files). 1. locate seq association in
1167      * current dataset/alignment context and add file as normal - keep handle of
1168      * any created ref objects. 2. decide if new data should be displayed : PDB
1169      * display: if alignment has PDB display already, should new pdb files be
1170      * aligned to it ?
1171      * 
1172      */
1173     // destPanel.adjustAnnotationHeight();
1174
1175   }
1176
1177   /**
1178    * split the given array of sequences into blocks of subsequences
1179    * corresponding to each visible contig
1180    * 
1181    * @param sequenceIs
1182    * @param contigs
1183    * @param gapChar
1184    *          padding character for ragged ends of visible contig region.
1185    * @return
1186    */
1187   private SequenceI[][] splitSeqsOnVisibleContigs(SequenceI[] sequenceIs,
1188           int[] contigs, char gapChar)
1189   {
1190     int nvc = contigs == null ? 1 : contigs.length / 2;
1191     SequenceI[][] blocks = new SequenceI[nvc][];
1192     if (contigs == null)
1193     {
1194       blocks[0] = new SequenceI[sequenceIs.length];
1195       System.arraycopy(sequenceIs, 0, blocks[0], 0, sequenceIs.length);
1196     }
1197     else
1198     {
1199       // deja vu - have I written this before ?? propagateGaps does this in a
1200       // way
1201       char[] gapset = null;
1202       int start = 0, width = 0;
1203       for (int c = 0; c < nvc; c++)
1204       {
1205         width = contigs[c * 2 + 1] - contigs[c * 2] + 1;
1206         for (int s = 0; s < sequenceIs.length; s++)
1207         {
1208           int end = sequenceIs[s].getLength();
1209           if (start < end)
1210           {
1211             if (start + width < end)
1212             {
1213               blocks[c][s] = sequenceIs[s].getSubSequence(start,
1214                       start + width);
1215             }
1216             else
1217             {
1218               blocks[c][s] = sequenceIs[s].getSubSequence(start, end);
1219               String sq = blocks[c][s].getSequenceAsString();
1220               for (int n = start + width; n > end; n--)
1221               {
1222                 sq += gapChar;
1223               }
1224             }
1225           }
1226           else
1227           {
1228             if (gapset == null || gapset.length < width)
1229             {
1230               char ng[] = new char[width];
1231               int gs = 0;
1232               if (gapset != null)
1233               {
1234                 System.arraycopy(gapset, 0, ng, 0, gs = gapset.length);
1235               }
1236               while (gs < ng.length)
1237               {
1238                 ng[gs++] = gapChar;
1239               }
1240             }
1241             blocks[c][s] = sequenceIs[s].getSubSequence(end, end);
1242             blocks[c][s].setSequence(gapset.toString().substring(0, width));
1243           }
1244         }
1245         if (c > 0)
1246         {
1247           // adjust window for next visible segnment
1248           start += contigs[c * 2 + 1] - contigs[c * 2];
1249         }
1250       }
1251     }
1252     return blocks;
1253   }
1254
1255   /**
1256    * recover corresponding sequence from original input data corresponding to
1257    * sequence in a specific job's input data.
1258    * 
1259    * @param oseq
1260    * @param sequenceIs
1261    * @param is
1262    * @param destAl
1263    * @return
1264    */
1265   private SequenceI getNewSeq(SequenceI oseq, SequenceI[] sequenceIs,
1266           int[] is, AlignmentI destAl)
1267   {
1268     int p = 0;
1269     while (p < sequenceIs.length && sequenceIs[p] != oseq)
1270     {
1271       p++;
1272     }
1273     if (p < sequenceIs.length && p < destAl.getHeight())
1274     {
1275       return destAl.getSequenceAt(is[p]);
1276     }
1277     return null;
1278   }
1279
1280   /**
1281    * 
1282    * @return true if the run method is safe to call
1283    */
1284   public boolean isValid()
1285   {
1286     ArrayList<String> _warnings = new ArrayList<>();
1287     boolean validt = true;
1288     if (jobs != null)
1289     {
1290       for (RestJob rj : (RestJob[]) jobs)
1291       {
1292         if (!rj.hasValidInput())
1293         {
1294           // invalid input for this job
1295           System.err.println("Job " + rj.getJobnum()
1296                   + " has invalid input. ( " + rj.getStatus() + ")");
1297           if (rj.hasStatus() && !_warnings.contains(rj.getStatus()))
1298           {
1299             _warnings.add(rj.getStatus());
1300           }
1301           validt = false;
1302         }
1303       }
1304     }
1305     if (!validt)
1306     {
1307       warnings = "";
1308       for (String st : _warnings)
1309       {
1310         if (warnings.length() > 0)
1311         {
1312           warnings += "\n";
1313         }
1314         warnings += st;
1315
1316       }
1317     }
1318     return validt;
1319   }
1320
1321   protected String warnings;
1322
1323   public boolean hasWarnings()
1324   {
1325     // TODO Auto-generated method stub
1326     return warnings != null && warnings.length() > 0;
1327   }
1328
1329   /**
1330    * get any informative messages about why the job thread couldn't start.
1331    * 
1332    * @return
1333    */
1334   public String getWarnings()
1335   {
1336     return isValid() ? "Job can be started. No warnings."
1337             : hasWarnings() ? warnings : "";
1338   }
1339
1340 }