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