JAL-1950 JAL-715 fall through for 201 ‘CREATED’ codes after job submission to get...
[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.ColumnSelection;
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(restClient.service
92                       .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(restClient.service
103                       .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(), input.getValue()
232                   .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
300         break;
301       default:
302         // Some other response. Probably need to pop up the content in a window.
303         // TODO: deal with all other HTTP response codes from server.
304         Cache.log.warn("Unhandled response status when " + getStage(stg)
305                 + "for " + postUrl + ": " + response.getStatusLine());
306         rj.error = true;
307         rj.setAllowedServerExceptions(0);
308         rj.setSubjobComplete(true);
309         rj.setSubmitted(true);
310         try
311         {
312           completeStatus(
313                   rj,
314                   response,
315                   ""
316                           + getStage(stg)
317                           + " resulted in an unexpected server response.<br/>Url concerned was <a href=\""
318                           + request.getURI()
319                           + "\">"
320                           + request.getURI()
321                           + "</a><br/>Filtered response content below:<br/>");
322         } catch (IOException e)
323         {
324           Cache.log.debug("IOException when consuming unhandled response",
325                   e);
326         }
327         ;
328       }
329     }
330   }
331
332   private void markAsFailed(RestJob rj, HttpResponse response)
333   {
334     // Failed.
335     rj.setSubmitted(true);
336     rj.setAllowedServerExceptions(0);
337     rj.setSubjobComplete(true);
338     rj.error = true;
339     rj.running = false;
340   }
341
342   /**
343    * set the jobRunning flag and post a link to the physical result page encoded
344    * in rj.getJobId()
345    * 
346    * @param rj
347    */
348   private void markJobAsRunning(RestJob rj)
349   {
350     rj.statMessage = "<br>Job submitted successfully. Results available at this URL:\n"
351             + "<a href="
352             + rj.getJobId()
353             + "\">"
354             + rj.getJobId()
355             + "</a><br>";
356     rj.running = true;
357   }
358
359   /**
360    * extract the job ID URL from the redirect page. Does nothing if job is
361    * already running.
362    * 
363    * @param rj
364    * @param response
365    */
366   private void extractJobId(RestJob rj, HttpResponse response)
367   {
368     Header[] loc;
369     if (!rj.isSubmitted())
370     {
371
372       // redirect URL - typical for IBIVU type jobs.
373       if ((loc = response.getHeaders(HTTPConstants.HEADER_LOCATION)) != null
374               && loc.length > 0)
375       {
376         if (loc.length > 1)
377         {
378           Cache.log
379                   .warn("Ignoring additional "
380                           + (loc.length - 1)
381                           + " location(s) provided in response header ( next one is '"
382                           + loc[1].getValue() + "' )");
383         }
384         rj.setJobId(loc[0].getValue());
385         rj.setSubmitted(true);
386       }
387     }
388   }
389
390   /**
391    * job has completed. Something valid should be available from con
392    * 
393    * @param rj
394    * @param con
395    * @param req
396    *          is a stateless request - expected to return the same data
397    *          regardless of how many times its called.
398    */
399   private void processResultSet(RestJob rj, HttpResponse con,
400           HttpRequestBase req)
401   {
402     if (rj.statMessage == null)
403     {
404       rj.statMessage = "";
405     }
406     rj.statMessage += "Job Complete.\n";
407     try
408     {
409       rj.resSet = new HttpResultSet(rj, con, req);
410       rj.gotresult = true;
411     } catch (IOException e)
412     {
413       rj.statMessage += "Couldn't parse results. Failed.";
414       rj.error = true;
415       rj.gotresult = false;
416     }
417   }
418
419   private void completeStatus(RestJob rj, HttpResponse con)
420           throws IOException
421   {
422     completeStatus(rj, con, null);
423
424   }
425
426   private void completeStatus(RestJob rj, HttpResponse con, String prefix)
427           throws IOException
428   {
429     StringBuffer sb = new StringBuffer();
430     if (prefix != null)
431     {
432       sb.append(prefix);
433     }
434     ;
435     if (rj.statMessage != null && rj.statMessage.length() > 0)
436     {
437       sb.append(rj.statMessage);
438     }
439     HttpEntity en = con.getEntity();
440     /*
441      * Just append the content as a string.
442      */
443     String f;
444     StringBuffer content = new StringBuffer(f = EntityUtils.toString(en));
445     f = f.toLowerCase();
446     int body = f.indexOf("<body");
447     if (body > -1)
448     {
449       content.delete(0, f.indexOf(">", body) + 1);
450     }
451     if (body > -1 && sb.length() > 0)
452     {
453       sb.append("\n");
454       content.insert(0, sb);
455       sb = null;
456     }
457     f = null;
458     rj.statMessage = content.toString();
459   }
460
461   @Override
462   public void pollJob(AWsJob job) throws Exception
463   {
464     assert (job instanceof RestJob);
465     System.err.println("Debug RestJob: Polling Job");
466     doPoll((RestJob) job);
467   }
468
469   @Override
470   public void StartJob(AWsJob job)
471   {
472     assert (job instanceof RestJob);
473     try
474     {
475       System.err.println("Debug RestJob: Posting Job");
476       doPost((RestJob) job);
477     } catch (NoValidInputDataException erex)
478     {
479       job.setSubjobComplete(true);
480       job.setSubmitted(true);
481       ((RestJob) job).statMessage = "<br>It looks like there was a problem with the data sent to the service :<br>"
482               + erex.getMessage() + "\n";
483       ((RestJob) job).error = true;
484
485     } catch (Exception ex)
486     {
487       job.setSubjobComplete(true);
488       job.setAllowedServerExceptions(-1);
489       Cache.log.error("Exception when trying to start Rest Job.", ex);
490     }
491   }
492
493   @Override
494   public void parseResult()
495   {
496     // crazy users will see this message
497     // TODO: finish this! and remove the message below!
498     Cache.log.warn("Rest job result parser is currently INCOMPLETE!");
499     int validres = 0;
500     for (RestJob rj : (RestJob[]) jobs)
501     {
502       if (rj.hasResponse() && rj.resSet != null && rj.resSet.isValid())
503       {
504         String ln = null;
505         try
506         {
507           Cache.log.debug("Parsing data for job " + rj.getJobId());
508           rj.parseResultSet();
509           if (rj.hasResults())
510           {
511             validres++;
512           }
513           Cache.log.debug("Finished parsing data for job " + rj.getJobId());
514
515         } catch (Error ex)
516         {
517           Cache.log.warn("Failed to finish parsing data for job "
518                   + rj.getJobId());
519           ex.printStackTrace();
520         } catch (Exception ex)
521         {
522           Cache.log.warn("Failed to finish parsing data for job "
523                   + rj.getJobId());
524           ex.printStackTrace();
525         } finally
526         {
527           rj.error = true;
528           rj.statMessage = "Error whilst parsing data for this job.<br>URL for job response is :<a href=\""
529                   + rj.resSet.getUrl()
530                   + "\">"
531                   + rj.resSet.getUrl()
532                   + "</a><br>";
533         }
534       }
535     }
536     if (validres > 0)
537     {
538       // add listeners and activate result display gui elements
539       /**
540        * decisions based on job result content + state of alignFrame that
541        * originated the job:
542        */
543       /*
544        * 1. Can/Should this job automatically open a new window for results
545        */
546       if (true)
547       {
548         // preserver current jalview behaviour
549         wsInfo.setViewResultsImmediatly(true);
550       }
551       else
552       {
553         // realiseResults(true, true);
554       }
555       // otherwise, should automatically view results
556
557       // TODO: check if at least one or more contexts are valid - if so, enable
558       // gui
559       wsInfo.showResultsNewFrame.addActionListener(new ActionListener()
560       {
561
562         @Override
563         public void actionPerformed(ActionEvent e)
564         {
565           realiseResults(false);
566         }
567
568       });
569       wsInfo.mergeResults.addActionListener(new ActionListener()
570       {
571
572         @Override
573         public void actionPerformed(ActionEvent e)
574         {
575           realiseResults(true);
576         }
577
578       });
579
580       wsInfo.setResultsReady();
581     }
582     else
583     {
584       // tell the user nothing was returned.
585       wsInfo.setStatus(wsInfo.STATE_STOPPED_ERROR);
586       wsInfo.setFinishedNoResults();
587     }
588   }
589
590   /**
591    * instructions for whether to create new alignment view on current alignment
592    * set, add to current set, or create new alignFrame
593    */
594   private enum AddDataTo
595   {
596     /**
597      * add annotation, trees etc to current view
598      */
599     currentView,
600     /**
601      * create a new view derived from current view and add data to that
602      */
603     newView,
604     /**
605      * create a new alignment frame for the result set and add annotation to
606      * that.
607      */
608     newAlignment
609   };
610
611   public void realiseResults(boolean merge)
612   {
613     /*
614      * 2. Should the job modify the parent alignment frame/view(s) (if they
615      * still exist and the alignment hasn't been edited) in order to display new
616      * annotation/features.
617      */
618     /**
619      * alignment panels derived from each alignment set returned by service.
620      */
621     ArrayList<jalview.gui.AlignmentPanel> destPanels = new ArrayList<jalview.gui.AlignmentPanel>();
622     /**
623      * list of instructions for how to process each distinct alignment set
624      * returned by the job set
625      */
626     ArrayList<AddDataTo> resultDest = new ArrayList<AddDataTo>();
627     /**
628      * when false, zeroth pane is panel derived from input deta.
629      */
630     boolean newAlignment = false;
631     /**
632      * gap character to be used for alignment reconstruction
633      */
634     char gapCharacter = restClient.av.getGapCharacter();
635     // Now, iterate over all alignment sets returned from all jobs:
636     // first inspect jobs and collate results data in order to count alignments
637     // and other results
638     // then assemble results from decomposed (v followed by h-separated) jobs
639     // finally, new views and alignments will be created and displayed as
640     // necessary.
641     boolean hsepjobs = restClient.service.isHseparable();
642     boolean vsepjobs = restClient.service.isVseparable();
643     // total number of distinct alignment sets generated by job set.
644     int numAlSets = 0, als = 0;
645     List<AlignmentI> destAls = new ArrayList<AlignmentI>();
646     List<jalview.datamodel.ColumnSelection> destColsel = new ArrayList<jalview.datamodel.ColumnSelection>();
647     List<List<NewickFile>> trees = new ArrayList<List<NewickFile>>();
648
649     do
650     {
651       // Step 1.
652       // iterate over each alignment set returned from each subjob. Treating
653       // each one returned in parallel with others.
654       // Result collation arrays
655
656       /**
657        * mapping between index of sequence in alignment that was submitted to
658        * server and index of sequence in the input alignment
659        */
660       int[][] ordermap = new int[jobs.length][];
661       SequenceI[][] rseqs = new SequenceI[jobs.length][];
662       AlignmentOrder[] orders = new AlignmentOrder[jobs.length];
663       AlignmentAnnotation[][] alan = new AlignmentAnnotation[jobs.length][];
664       SequenceGroup[][] sgrp = new SequenceGroup[jobs.length][];
665       // Now collect all data for alignment Set als from job array
666       for (int j = 0; j < jobs.length; j++)
667       {
668         RestJob rj = (RestJob) jobs[j];
669         if (rj.hasResults())
670         {
671           JalviewDataset rset = rj.context;
672           if (rset.hasAlignments())
673           {
674             if (numAlSets < rset.getAl().size())
675             {
676               numAlSets = rset.getAl().size();
677             }
678             if (als < rset.getAl().size()
679                     && rset.getAl().get(als).isModified())
680             {
681               // Collate result data
682               // TODO: decide if all alignmentI should be collected rather than
683               // specific alignment data containers
684               // for moment, we just extract content, but this means any
685               // alignment properties may be lost.
686               AlignmentSet alset = rset.getAl().get(als);
687               alan[j] = alset.al.getAlignmentAnnotation();
688               if (alset.al.getGroups() != null)
689               {
690                 sgrp[j] = new SequenceGroup[alset.al.getGroups().size()];
691                 alset.al.getGroups().toArray(sgrp[j]);
692               }
693               else
694               {
695                 sgrp[j] = null;
696               }
697               orders[j] = new AlignmentOrder(alset.al);
698               rseqs[j] = alset.al.getSequencesArray();
699               ordermap[j] = rj.getOrderMap();
700               // if (rj.isInputUniquified()) {
701               // jalview.analysis.AlignmentSorter.recoverOrder(rseqs[als]);
702               // }
703
704               if (alset.trees != null)
705               {
706                 trees.add(new ArrayList<NewickFile>(alset.trees));
707               }
708               else
709               {
710                 trees.add(new ArrayList<NewickFile>());
711               }
712             }
713           }
714         }
715       }
716       // Now aggregate and present results from this frame of alignment data.
717       int nvertsep = 0, nvertseps = 1;
718       if (vsepjobs)
719       {
720         // Jobs relate to different rows of input alignment.
721         // Jobs are subdivided by rows before columns,
722         // so there will now be a number of subjobs according tohsep for each
723         // vertsep
724         // TODO: get vertical separation intervals for each job and set
725         // nvertseps
726         // TODO: merge data from each group/sequence onto whole
727         // alignment
728       }
729       /**
730        * index into rest jobs subdivided vertically
731        */
732       int vrestjob = 0;
733       // Destination alignments for all result data.
734       ArrayList<SequenceGroup> visgrps = new ArrayList<SequenceGroup>();
735       Hashtable<String, SequenceGroup> groupNames = new Hashtable<String, SequenceGroup>();
736       ArrayList<AlignmentAnnotation> visAlAn = null;
737       for (nvertsep = 0; nvertsep < nvertseps; nvertsep++)
738       {
739         // TODO: set scope w.r.t. original alignment view for vertical
740         // separation.
741         {
742           // results for a job exclude hidden columns of input data, so map
743           // back on to all visible regions
744           /**
745            * rest job result we are working with
746            */
747           int nrj = vrestjob;
748
749           RestJob rj = (RestJob) jobs[nrj];
750           int contigs[] = input.getVisibleContigs();
751           AlignmentI destAl = null;
752           jalview.datamodel.ColumnSelection destCs = null;
753           // Resolve destAl for this data.
754           if (als == 0 && rj.isInputContextModified())
755           {
756             // special case: transfer features, annotation, groups, etc,
757             // from input
758             // context to align panel derived from input data
759             if (destAls.size() > als)
760             {
761               destAl = destAls.get(als);
762             }
763             else
764             {
765               if (!restClient.isAlignmentModified() && merge)
766               {
767                 destAl = restClient.av.getAlignment();
768                 destCs = restClient.av.getColumnSelection();
769                 resultDest
770                         .add(restClient.isShowResultsInNewView() ? AddDataTo.newView
771                                 : AddDataTo.currentView);
772                 destPanels.add(restClient.recoverAlignPanelForView());
773               }
774               else
775               {
776                 newAlignment = true;
777                 // recreate the input alignment data
778                 Object[] idat = input
779                         .getAlignmentAndColumnSelection(gapCharacter);
780                 destAl = new Alignment((SequenceI[]) idat[0]);
781                 destCs = (ColumnSelection) idat[1];
782                 resultDest.add(AddDataTo.newAlignment);
783                 // but do not add to the alignment panel list - since we need to
784                 // create a whole new alignFrame set.
785               }
786               destAls.add(destAl);
787               destColsel.add(destCs);
788             }
789           }
790           else
791           {
792             // alignment(s) returned by service is to be re-integrated and
793             // displayed
794             if (destAls.size() > als)
795             {
796               if (!vsepjobs)
797               {
798                 // TODO: decide if multiple multiple alignments returned by
799                 // non-vseparable services are allowed.
800                 Cache.log
801                         .warn("dealing with multiple alignment products returned by non-vertically separable service.");
802               }
803               // recover reference to last alignment created for this rest frame
804               // ready for extension
805               destAl = destAls.get(als);
806               destCs = destColsel.get(als);
807             }
808             else
809             {
810               Object[] newview;
811
812               if (!hsepjobs)
813               {
814                 // single alignment for any job that gets mapped back on to
815                 // input data. Reconstruct by interleaving parts of returned
816                 // alignment with hidden parts of input data.
817                 SequenceI[][] nsq = splitSeqsOnVisibleContigs(rseqs[nrj],
818                         contigs, gapCharacter);
819                 AlignmentOrder alo[] = new AlignmentOrder[nsq.length];
820                 for (int no = 0; no < alo.length; no++)
821                 {
822                   alo[no] = new AlignmentOrder(orders[nrj].getOrder());
823                 }
824                 newview = input.getUpdatedView(nsq, orders, gapCharacter);
825               }
826               else
827               {
828                 // each job maps to a single visible contig, and all need to be
829                 // stitched back together.
830                 // reconstruct using sub-region based MSA alignment construction
831                 // mechanism
832                 newview = input.getUpdatedView(rseqs, orders, gapCharacter);
833               }
834               destAl = new Alignment((SequenceI[]) newview[0]);
835               destCs = (ColumnSelection) newview[1];
836               newAlignment = true;
837               // TODO create alignment from result data with propagated
838               // references.
839               destAls.add(destAl);
840               destColsel.add(destCs);
841               resultDest.add(AddDataTo.newAlignment);
842               throw new Error(
843                       MessageManager
844                               .getString("error.implementation_error")
845                               + "TODO: ");
846             }
847           }
848           /**
849            * save initial job in this set in case alignment is h-separable
850            */
851           int initnrj = nrj;
852           // Now add in groups
853           for (int ncnt = 0; ncnt < contigs.length; ncnt += 2)
854           {
855             if (!hsepjobs)
856             {
857               // single alignment for any job that gets mapped back on to input
858               // data.
859             }
860             else
861             {
862               // each job maps to a single visible contig, and all need to be
863               // stitched back together.
864               if (ncnt > 0)
865               {
866                 nrj++;
867               }
868               // TODO: apply options for group merging and annotation merging.
869               // If merging not supported, then either clear hashtables now or
870               // use them to rename the new annotation/groups for each contig if
871               // a conflict occurs.
872             }
873             if (sgrp[nrj] != null)
874             {
875               for (SequenceGroup sg : sgrp[nrj])
876               {
877                 boolean recovered = false;
878                 SequenceGroup exsg = null;
879                 if (sg.getName() != null)
880                 {
881                   exsg = groupNames.get(sg.getName());
882                 }
883                 if (exsg == null)
884                 {
885                   exsg = new SequenceGroup(sg);
886                   groupNames.put(exsg.getName(), exsg);
887                   visgrps.add(exsg);
888                   exsg.setStartRes(sg.getStartRes() + contigs[ncnt]);
889                   exsg.setEndRes(sg.getEndRes() + contigs[ncnt]);
890                 }
891                 else
892                 {
893                   recovered = true;
894                 }
895                 // now replace any references from the result set with
896                 // corresponding refs from alignment input set.
897
898                 // TODO: cope with recovering hidden sequences from
899                 // resultContext
900                 {
901                   for (SequenceI oseq : sg.getSequences(null))
902                   {
903                     SequenceI nseq = getNewSeq(oseq, rseqs[nrj],
904                             ordermap[nrj], destAl);
905                     if (nseq != null)
906                     {
907                       if (!recovered)
908                       {
909                         exsg.deleteSequence(oseq, false);
910                       }
911                       exsg.addSequence(nseq, false);
912                     }
913                     else
914                     {
915                       Cache.log
916                               .warn("Couldn't resolve original sequence for new sequence.");
917                     }
918                   }
919                   if (sg.hasSeqrep())
920                   {
921                     if (exsg.getSeqrep() == sg.getSeqrep())
922                     {
923                       // lift over sequence rep reference too
924                       SequenceI oseq = sg.getSeqrep();
925                       SequenceI nseq = getNewSeq(oseq, rseqs[nrj],
926                               ordermap[nrj], destAl);
927                       if (nseq != null)
928                       {
929                         exsg.setSeqrep(nseq);
930                       }
931                     }
932                   }
933                 }
934                 if (recovered)
935                 {
936                   // adjust boundaries of recovered group w.r.t. new group being
937                   // merged on to original alignment.
938                   int start = sg.getStartRes() + contigs[ncnt], end = sg
939                           .getEndRes() + contigs[ncnt];
940                   if (start < exsg.getStartRes())
941                   {
942                     exsg.setStartRes(start);
943                   }
944                   if (end > exsg.getEndRes())
945                   {
946                     exsg.setEndRes(end);
947                   }
948                 }
949               }
950             }
951           }
952           // reset job counter
953           nrj = initnrj;
954           // and finally add in annotation and any trees for each job
955           for (int ncnt = 0; ncnt < contigs.length; ncnt += 2)
956           {
957             if (!hsepjobs)
958             {
959               // single alignment for any job that gets mapped back on to input
960               // data.
961             }
962             else
963             {
964               // each job maps to a single visible contig, and all need to be
965               // stitched back together.
966               if (ncnt > 0)
967               {
968                 nrj++;
969               }
970             }
971
972             // merge alignmentAnnotation into one row
973             if (alan[nrj] != null)
974             {
975               for (int an = 0; an < alan[nrj].length; an++)
976               {
977                 SequenceI sqass = null;
978                 SequenceGroup grass = null;
979                 if (alan[nrj][an].sequenceRef != null)
980                 {
981                   // TODO: ensure this relocates sequence reference to local
982                   // context.
983                   sqass = getNewSeq(alan[nrj][an].sequenceRef, rseqs[nrj],
984                           ordermap[nrj], destAl);
985                 }
986                 if (alan[nrj][an].groupRef != null)
987                 {
988                   // TODO: verify relocate group reference to local context
989                   // works correctly
990                   grass = groupNames.get(alan[nrj][an].groupRef.getName());
991                   if (grass == null)
992                   {
993                     Cache.log
994                             .error("Couldn't relocate group referemce for group "
995                                     + alan[nrj][an].groupRef.getName());
996                   }
997                 }
998                 if (visAlAn == null)
999                 {
1000                   visAlAn = new ArrayList<AlignmentAnnotation>();
1001                 }
1002                 AlignmentAnnotation visan = null;
1003                 for (AlignmentAnnotation v : visAlAn)
1004                 {
1005                   if (v.label != null
1006                           && v.label.equals(alan[nrj][an].label))
1007                   {
1008                     visan = v;
1009                   }
1010                 }
1011                 if (visan == null)
1012                 {
1013                   visan = new AlignmentAnnotation(alan[nrj][an]);
1014                   // copy annotations, and wipe out/update refs.
1015                   visan.annotations = new Annotation[input.getWidth()];
1016                   visan.groupRef = grass;
1017                   visan.sequenceRef = sqass;
1018                   visAlAn.add(visan);
1019                 }
1020                 if (contigs[ncnt] + alan[nrj][an].annotations.length > visan.annotations.length)
1021                 {
1022                   // increase width of annotation row
1023                   Annotation[] newannv = new Annotation[contigs[ncnt]
1024                           + alan[nrj][an].annotations.length];
1025                   System.arraycopy(visan.annotations, 0, newannv, 0,
1026                           visan.annotations.length);
1027                   visan.annotations = newannv;
1028                 }
1029                 // now copy local annotation data into correct position
1030                 System.arraycopy(alan[nrj][an].annotations, 0,
1031                         visan.annotations, contigs[ncnt],
1032                         alan[nrj][an].annotations.length);
1033
1034               }
1035             }
1036             // Trees
1037             if (trees.get(nrj).size() > 0)
1038             {
1039               for (NewickFile nf : trees.get(nrj))
1040               {
1041                 // TODO: process each newick file, lifting over sequence refs to
1042                 // current alignment, if necessary.
1043                 Cache.log
1044                         .error("Tree recovery from restjob not yet implemented.");
1045               }
1046             }
1047           }
1048         }
1049       } // end of vseps loops.
1050       if (visAlAn != null)
1051       {
1052         for (AlignmentAnnotation v : visAlAn)
1053         {
1054           destAls.get(als).addAnnotation(v);
1055         }
1056       }
1057       if (visgrps != null)
1058       {
1059         for (SequenceGroup sg : visgrps)
1060         {
1061           destAls.get(als).addGroup(sg);
1062         }
1063       }
1064     } while (++als < numAlSets);
1065     // Finally, assemble each new alignment, and create new gui components to
1066     // present it.
1067     /**
1068      * current AlignFrame where results will go.
1069      */
1070     AlignFrame destaf = restClient.recoverAlignFrameForView();
1071     /**
1072      * current pane being worked with
1073      */
1074     jalview.gui.AlignmentPanel destPanel = restClient
1075             .recoverAlignPanelForView();
1076     als = 0;
1077     for (AddDataTo action : resultDest)
1078     {
1079       AlignmentI destal;
1080       ColumnSelection destcs;
1081       String alTitle = MessageManager.formatMessage(
1082               "label.webservice_job_title_on", new String[] {
1083                   restClient.service.details.Action,
1084                   restClient.service.details.Name, restClient.viewTitle });
1085       switch (action)
1086       {
1087       case newAlignment:
1088         destal = destAls.get(als);
1089         destcs = destColsel.get(als);
1090         destaf = new AlignFrame(destal, destcs, AlignFrame.DEFAULT_WIDTH,
1091                 AlignFrame.DEFAULT_HEIGHT);
1092         PaintRefresher.Refresh(destaf, destaf.getViewport()
1093                 .getSequenceSetId());
1094         // todo transfer any feature settings and colouring
1095         /*
1096          * destaf.getFeatureRenderer().transferSettings(this.featureSettings);
1097          * // update orders if (alorders.size() > 0) { if (alorders.size() == 1)
1098          * { af.addSortByOrderMenuItem(WebServiceName + " Ordering",
1099          * (AlignmentOrder) alorders.get(0)); } else { // construct a
1100          * non-redundant ordering set Vector names = new Vector(); for (int i =
1101          * 0, l = alorders.size(); i < l; i++) { String orderName = new
1102          * String(" Region " + i); int j = i + 1;
1103          * 
1104          * while (j < l) { if (((AlignmentOrder) alorders.get(i))
1105          * .equals(((AlignmentOrder) alorders.get(j)))) { alorders.remove(j);
1106          * l--; orderName += "," + j; } else { j++; } }
1107          * 
1108          * if (i == 0 && j == 1) { names.add(new String("")); } else {
1109          * names.add(orderName); } } for (int i = 0, l = alorders.size(); i < l;
1110          * i++) { af.addSortByOrderMenuItem( WebServiceName + ((String)
1111          * names.get(i)) + " Ordering", (AlignmentOrder) alorders.get(i)); } } }
1112          */
1113         // TODO: modify this and previous alignment's title if many alignments
1114         // have been returned.
1115         Desktop.addInternalFrame(destaf, alTitle, AlignFrame.DEFAULT_WIDTH,
1116                 AlignFrame.DEFAULT_HEIGHT);
1117
1118         break;
1119       case newView:
1120         // TODO: determine title for view
1121         break;
1122       case currentView:
1123         break;
1124       }
1125     }
1126     if (!newAlignment)
1127     {
1128       if (restClient.isShowResultsInNewView())
1129       {
1130         // destPanel = destPanel.alignFrame.newView(false);
1131       }
1132     }
1133     else
1134     {
1135
1136     }
1137     /*
1138      * if (als) // add the destination panel to frame zero of result panel set }
1139      * } if (destPanels.size()==0) { AlignFrame af = new AlignFrame((AlignmentI)
1140      * idat[0], (ColumnSelection) idat[1], AlignFrame.DEFAULT_WIDTH,
1141      * AlignFrame.DEFAULT_HEIGHT);
1142      * 
1143      * jalview.gui.Desktop.addInternalFrame(af, "Results for " +
1144      * restClient.service.details.Name + " " + restClient.service.details.Action
1145      * + " on " + restClient.af.getTitle(), AlignFrame.DEFAULT_WIDTH,
1146      * AlignFrame.DEFAULT_HEIGHT); destPanel = af.alignPanel; // create totally
1147      * new alignment from stashed data/results
1148      */
1149
1150     /*
1151      */
1152
1153     /**
1154      * alignments. New alignments are added to dataset, and subsequently
1155      * annotated/visualised accordingly. 1. New alignment frame created for new
1156      * alignment. Decide if any vis settings should be inherited from old
1157      * alignment frame (e.g. sequence ordering ?). 2. Subsequent data added to
1158      * alignment as below:
1159      */
1160     /**
1161      * annotation update to original/newly created context alignment: 1.
1162      * identify alignment where annotation is to be loaded onto. 2. Add
1163      * annotation, excluding any duplicates. 3. Ensure annotation is visible on
1164      * alignment - honouring ordering given by file.
1165      */
1166     /**
1167      * features updated to original or newly created context alignment: 1.
1168      * Features are(or were already) added to dataset. 2. Feature settings
1169      * modified to ensure all features are displayed - honouring any ordering
1170      * given by result file. Consider merging action with the code used by the
1171      * DAS fetcher to update alignment views with new info.
1172      */
1173     /**
1174      * Seq associated data files (PDB files). 1. locate seq association in
1175      * current dataset/alignment context and add file as normal - keep handle of
1176      * any created ref objects. 2. decide if new data should be displayed : PDB
1177      * display: if alignment has PDB display already, should new pdb files be
1178      * aligned to it ?
1179      * 
1180      */
1181     // destPanel.adjustAnnotationHeight();
1182
1183   }
1184
1185   /**
1186    * split the given array of sequences into blocks of subsequences
1187    * corresponding to each visible contig
1188    * 
1189    * @param sequenceIs
1190    * @param contigs
1191    * @param gapChar
1192    *          padding character for ragged ends of visible contig region.
1193    * @return
1194    */
1195   private SequenceI[][] splitSeqsOnVisibleContigs(SequenceI[] sequenceIs,
1196           int[] contigs, char gapChar)
1197   {
1198     int nvc = contigs == null ? 1 : contigs.length / 2;
1199     SequenceI[][] blocks = new SequenceI[nvc][];
1200     if (contigs == null)
1201     {
1202       blocks[0] = new SequenceI[sequenceIs.length];
1203       System.arraycopy(sequenceIs, 0, blocks[0], 0, sequenceIs.length);
1204     }
1205     else
1206     {
1207       // deja vu - have I written this before ?? propagateGaps does this in a
1208       // way
1209       char[] gapset = null;
1210       int start = 0, width = 0;
1211       for (int c = 0; c < nvc; c++)
1212       {
1213         width = contigs[c * 2 + 1] - contigs[c * 2] + 1;
1214         for (int s = 0; s < sequenceIs.length; s++)
1215         {
1216           int end = sequenceIs[s].getLength();
1217           if (start < end)
1218           {
1219             if (start + width < end)
1220             {
1221               blocks[c][s] = sequenceIs[s].getSubSequence(start, start
1222                       + width);
1223             }
1224             else
1225             {
1226               blocks[c][s] = sequenceIs[s].getSubSequence(start, end);
1227               String sq = blocks[c][s].getSequenceAsString();
1228               for (int n = start + width; n > end; n--)
1229               {
1230                 sq += gapChar;
1231               }
1232             }
1233           }
1234           else
1235           {
1236             if (gapset == null || gapset.length < width)
1237             {
1238               char ng[] = new char[width];
1239               int gs = 0;
1240               if (gapset != null)
1241               {
1242                 System.arraycopy(gapset, 0, ng, 0, gs = gapset.length);
1243               }
1244               while (gs < ng.length)
1245               {
1246                 ng[gs++] = gapChar;
1247               }
1248             }
1249             blocks[c][s] = sequenceIs[s].getSubSequence(end, end);
1250             blocks[c][s].setSequence(gapset.toString().substring(0, width));
1251           }
1252         }
1253         if (c > 0)
1254         {
1255           // adjust window for next visible segnment
1256           start += contigs[c * 2 + 1] - contigs[c * 2];
1257         }
1258       }
1259     }
1260     return blocks;
1261   }
1262
1263   /**
1264    * recover corresponding sequence from original input data corresponding to
1265    * sequence in a specific job's input data.
1266    * 
1267    * @param oseq
1268    * @param sequenceIs
1269    * @param is
1270    * @param destAl
1271    * @return
1272    */
1273   private SequenceI getNewSeq(SequenceI oseq, SequenceI[] sequenceIs,
1274           int[] is, AlignmentI destAl)
1275   {
1276     int p = 0;
1277     while (p < sequenceIs.length && sequenceIs[p] != oseq)
1278     {
1279       p++;
1280     }
1281     if (p < sequenceIs.length && p < destAl.getHeight())
1282     {
1283       return destAl.getSequenceAt(is[p]);
1284     }
1285     return null;
1286   }
1287
1288   /**
1289    * 
1290    * @return true if the run method is safe to call
1291    */
1292   public boolean isValid()
1293   {
1294     ArrayList<String> _warnings = new ArrayList<String>();
1295     boolean validt = true;
1296     if (jobs != null)
1297     {
1298       for (RestJob rj : (RestJob[]) jobs)
1299       {
1300         if (!rj.hasValidInput())
1301         {
1302           // invalid input for this job
1303           System.err.println("Job " + rj.getJobnum()
1304                   + " has invalid input. ( " + rj.getStatus() + ")");
1305           if (rj.hasStatus() && !_warnings.contains(rj.getStatus()))
1306           {
1307             _warnings.add(rj.getStatus());
1308           }
1309           validt = false;
1310         }
1311       }
1312     }
1313     if (!validt)
1314     {
1315       warnings = "";
1316       for (String st : _warnings)
1317       {
1318         if (warnings.length() > 0)
1319         {
1320           warnings += "\n";
1321         }
1322         warnings += st;
1323
1324       }
1325     }
1326     return validt;
1327   }
1328
1329   protected String warnings;
1330
1331   public boolean hasWarnings()
1332   {
1333     // TODO Auto-generated method stub
1334     return warnings != null && warnings.length() > 0;
1335   }
1336
1337   /**
1338    * get any informative messages about why the job thread couldn't start.
1339    * 
1340    * @return
1341    */
1342   public String getWarnings()
1343   {
1344     return isValid() ? "Job can be started. No warnings."
1345             : hasWarnings() ? warnings : "";
1346   }
1347
1348 }