JAL-3878 Split web services menu entries by programs and hosts.
[jalview.git] / src / jalview / ws2 / operations / AlignmentOperation.java
1 package jalview.ws2.operations;
2
3 import static java.lang.String.format;
4
5 import java.awt.event.MouseAdapter;
6 import java.awt.event.MouseEvent;
7 import java.io.IOException;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.HashMap;
11 import java.util.Hashtable;
12 import java.util.LinkedHashMap;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Objects;
16 import java.util.concurrent.CompletionStage;
17
18 import javax.swing.JMenu;
19 import javax.swing.JMenuItem;
20 import javax.swing.ToolTipManager;
21
22 import jalview.analysis.AlignSeq;
23 import jalview.analysis.AlignmentSorter;
24 import jalview.analysis.SeqsetUtils;
25 import jalview.bin.Cache;
26 import jalview.datamodel.AlignedCodonFrame;
27 import jalview.datamodel.Alignment;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.AlignmentOrder;
30 import jalview.datamodel.AlignmentView;
31 import jalview.datamodel.HiddenColumns;
32 import jalview.datamodel.SequenceI;
33 import jalview.datamodel.Sequence;
34 import jalview.gui.AlignFrame;
35 import jalview.gui.AlignViewport;
36 import jalview.gui.Desktop;
37 import jalview.gui.JvSwingUtils;
38 import jalview.gui.WebserviceInfo;
39 import jalview.gui.WsJobParameters;
40 import jalview.util.MathUtils;
41 import jalview.util.MessageManager;
42 import jalview.ws.params.ArgumentI;
43 import jalview.ws.params.WsParamSetI;
44 import jalview.ws2.MenuEntryProviderI;
45 import jalview.ws2.ResultSupplier;
46 import jalview.ws2.WSJob;
47 import jalview.ws2.WSJobStatus;
48 import jalview.ws2.WebServiceExecutor;
49 import jalview.ws2.WebServiceI;
50 import jalview.ws2.WebServiceInfoUpdater;
51 import jalview.ws2.WebServiceWorkerI;
52 import jalview.ws2.utils.WSJobList;
53
54 /**
55  *
56  * @author mmwarowny
57  *
58  */
59 public class AlignmentOperation implements Operation
60 {
61   final WebServiceI service;
62
63   final ResultSupplier<AlignmentI> supplier;
64
65   public AlignmentOperation(WebServiceI service,
66           ResultSupplier<AlignmentI> supplier)
67   {
68     this.service = service;
69     this.supplier = supplier;
70   }
71
72   @Override
73   public String getName()
74   {
75     return service.getName();
76   }
77
78   @Override
79   public String getTypeName()
80   {
81     return "Multiple Sequence Alignment";
82   }
83
84   @Override
85   public String getHostName()
86   {
87     return service.getHostName();
88   }
89
90   @Override
91   public int getMinSequences()
92   {
93     return 2;
94   }
95
96   @Override
97   public int getMaxSequences()
98   {
99     return Integer.MAX_VALUE;
100   }
101
102   @Override
103   public boolean isProteinOperation()
104   {
105     return true;
106   }
107
108   @Override
109   public boolean isNucleotideOperation()
110   {
111     return true;
112   }
113
114   @Override
115   public boolean canSubmitGaps()
116   {
117     // hack copied from original jabaws code, don't blame me
118     return service.getName().contains("lustal");
119   }
120
121   @Override
122   public boolean isInteractive()
123   {
124     return false;
125   }
126
127   @Override
128   public MenuEntryProviderI getMenuBuilder()
129   {
130     return this::buildMenu;
131   }
132
133   protected void buildMenu(JMenu parent, AlignFrame frame)
134   {
135     if (canSubmitGaps())
136     {
137       var alignSubmenu = new JMenu(service.getName());
138       buildMenu(alignSubmenu, frame, false);
139       parent.add(alignSubmenu);
140       var realignSubmenu = new JMenu(MessageManager.formatMessage(
141               "label.realign_with_params", service.getName()));
142       realignSubmenu.setToolTipText(MessageManager
143               .getString("label.align_sequences_to_existing_alignment"));
144       buildMenu(realignSubmenu, frame, true);
145       parent.add(realignSubmenu);
146     }
147     else
148     {
149       buildMenu(parent, frame, false);
150     }
151   }
152
153   protected void buildMenu(JMenu parent, AlignFrame frame,
154           boolean submitGaps)
155   {
156     final String action = submitGaps ? "Align" : "Realign";
157     final var calcName = service.getName();
158
159     final AlignmentView msa = frame.gatherSequencesForAlignment();
160     final AlignViewport viewport = frame.getViewport();
161     final AlignmentI alignment = frame.getViewport().getAlignment();
162     String title = frame.getTitle();
163     WebServiceExecutor executor = frame.getViewport().getWSExecutor();
164     {
165       var item = new JMenuItem(MessageManager.formatMessage(
166               "label.calcname_with_default_settings", calcName));
167       item.setToolTipText(MessageManager
168               .formatMessage("label.action_with_default_settings", action));
169       item.addActionListener((event) -> {
170         if (msa != null)
171         {
172           WebServiceWorkerI worker = new AlignmentWorker(msa,
173                   Collections.emptyList(), title, submitGaps, true,
174                   alignment, viewport);
175           executor.submit(worker);
176         }
177       });
178       parent.add(item);
179     }
180
181     if (service.hasParameters())
182     {
183       var item = new JMenuItem(
184               MessageManager.getString("label.edit_settings_and_run"));
185       item.setToolTipText(MessageManager.getString(
186               "label.view_and_change_parameters_before_alignment"));
187       item.addActionListener((event) -> {
188         if (msa != null)
189         {
190           openEditParamsDialog(service, null, null)
191                   .thenAcceptAsync((arguments) -> {
192                     if (arguments != null)
193                     {
194                       WebServiceWorkerI worker = new AlignmentWorker(msa,
195                               arguments, title, submitGaps, true, alignment,
196                               viewport);
197                       executor.submit(worker);
198                     }
199                   });
200         }
201       });
202       parent.add(item);
203     }
204
205     var presets = service.getParamStore().getPresets();
206     if (presets != null && presets.size() > 0)
207     {
208       final var presetList = new JMenu(MessageManager
209               .formatMessage("label.run_with_preset_params", calcName));
210       final var showToolTipFor = ToolTipManager.sharedInstance()
211               .getDismissDelay();
212       for (final var preset : presets)
213       {
214         var item = new JMenuItem(preset.getName());
215         final int QUICK_TOOLTIP = 1500;
216         item.addMouseListener(new MouseAdapter()
217         {
218           @Override
219           public void mouseEntered(MouseEvent e)
220           {
221             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
222           }
223
224           @Override
225           public void mouseExited(MouseEvent e)
226           {
227             ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
228           }
229         });
230         String tooltip = JvSwingUtils.wrapTooltip(true,
231                 format("<strong>%s</strong><br/>%s",
232                         MessageManager.getString(
233                                 preset.isModifiable() ? "label.user_preset"
234                                         : "label.service_preset"),
235                         preset.getDescription()));
236         item.setToolTipText(tooltip);
237         item.addActionListener((event) -> {
238           if (msa != null)
239           {
240             WebServiceWorkerI worker = new AlignmentWorker(msa,
241                     preset.getArguments(), title, submitGaps, true,
242                     alignment, viewport);
243             executor.submit(worker);
244           }
245         });
246         presetList.add(item);
247       }
248       parent.add(presetList);
249     }
250   }
251
252   private CompletionStage<List<ArgumentI>> openEditParamsDialog(
253           WebServiceI service, WsParamSetI preset,
254           List<ArgumentI> arguments)
255   {
256     WsJobParameters jobParams;
257     if (preset == null && arguments != null && arguments.size() > 0)
258       jobParams = new WsJobParameters(service.getParamStore(), preset,
259               arguments);
260     else
261       jobParams = new WsJobParameters(service.getParamStore(), preset,
262               null);
263     var stage = jobParams.showRunDialog();
264     return stage.thenApply((startJob) -> {
265       if (startJob)
266       {
267         if (jobParams.getPreset() == null)
268         {
269           return jobParams.getJobParams();
270         }
271         else
272         {
273           return jobParams.getPreset().getArguments();
274         }
275       }
276       else
277       {
278         return null;
279       }
280     });
281   }
282
283   /**
284    * Implementation of the web service worker performing multiple sequence
285    * alignment.
286    *
287    * @author mmwarowny
288    *
289    */
290   private class AlignmentWorker implements WebServiceWorkerI
291   {
292
293     private long uid = MathUtils.getUID();
294
295     private final AlignmentView msa;
296
297     private final AlignmentI dataset;
298
299     private final List<AlignedCodonFrame> codonFrame = new ArrayList<>();
300
301     private List<ArgumentI> args = Collections.emptyList();
302
303     private String alnTitle = "";
304
305     private boolean submitGaps = false;
306
307     private boolean preserveOrder = false;
308
309     private char gapCharacter;
310
311     private WSJobList jobs = new WSJobList();
312
313     private Map<Long, JobInput> inputs = new LinkedHashMap<>();
314
315     private WebserviceInfo wsInfo;
316
317     private Map<Long, Integer> exceptionCount = new HashMap<>();
318
319     private final int MAX_RETRY = 5;
320
321     AlignmentWorker(AlignmentView msa, List<ArgumentI> args,
322             String alnTitle, boolean submitGaps, boolean preserveOrder,
323             AlignmentI alignment, AlignViewport viewport)
324     {
325       this.msa = msa;
326       this.dataset = alignment.getDataset();
327       List<AlignedCodonFrame> cf = Objects.requireNonNullElse(
328               alignment.getCodonFrames(), Collections.emptyList());
329       this.codonFrame.addAll(cf);
330       this.args = args;
331       this.alnTitle = alnTitle;
332       this.submitGaps = submitGaps;
333       this.preserveOrder = preserveOrder;
334       this.gapCharacter = viewport.getGapCharacter();
335
336       String panelInfo = String.format("%s using service hosted at %s%n%s",
337               service.getName(), service.getHostName(),
338               Objects.requireNonNullElse(service.getDescription(), ""));
339       wsInfo = new WebserviceInfo(service.getName(), panelInfo, false);
340     }
341
342     @Override
343     public long getUID()
344     {
345       return uid;
346     }
347
348     @Override
349     public WebServiceI getWebService()
350     {
351       return service;
352     }
353
354     @Override
355     public List<WSJob> getJobs()
356     {
357       return Collections.unmodifiableList(jobs);
358     }
359
360     @Override
361     public void startJobs() throws IOException
362     {
363       String outputHeader = String.format("%s of %s%nJob details%n",
364               submitGaps ? "Re-alignment" : "Alignment", alnTitle);
365       SequenceI[][] conmsa = msa.getVisibleContigs('-');
366       if (conmsa == null)
367       {
368         return;
369       }
370       WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo);
371       updater.setOutputHeader(outputHeader);
372       int numValid = 0;
373       for (int i = 0; i < conmsa.length; i++)
374       {
375         JobInput input = JobInput.create(conmsa[i], 2, submitGaps);
376         WSJob job = new WSJob(service.getProviderName(), service.getName(),
377                 service.getHostName());
378         job.setJobNum(wsInfo.addJobPane());
379         if (conmsa.length > 0)
380         {
381           wsInfo.setProgressName(String.format("region %d", i),
382                   job.getJobNum());
383         }
384         wsInfo.setProgressText(job.getJobNum(), outputHeader);
385         job.addPropertyChangeListener(updater);
386         inputs.put(job.getUid(), input);
387         jobs.add(job);
388         if (input.isInputValid())
389         {
390           int count;
391           String jobId = null;
392           do
393           {
394             count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY);
395             try
396             {
397               jobId = service.submit(input.inputSequences, args);
398               Cache.log.debug((format("Job %s submitted", job)));
399               exceptionCount.remove(job.getUid());
400             } catch (IOException e)
401             {
402               exceptionCount.put(job.getUid(), --count);
403             }
404           } while (jobId == null && count > 0);
405           if (jobId != null)
406           {
407             job.setJobId(jobId);
408             job.setStatus(WSJobStatus.SUBMITTED);
409             numValid++;
410           }
411           else
412           {
413             job.setStatus(WSJobStatus.SERVER_ERROR);
414           }
415         }
416         else
417         {
418           job.setStatus(WSJobStatus.INVALID);
419           job.setErrorLog(
420                   MessageManager.getString("label.empty_alignment_job"));
421         }
422       }
423       if (numValid > 0)
424       {
425         // wsInfo.setThisService() should happen here
426         wsInfo.setVisible(true);
427       }
428       else
429       {
430         wsInfo.setVisible(false);
431         // TODO show notification dialog.
432         // JvOptionPane.showMessageDialog(frame,
433         // MessageManager.getString("info.invalid_msa_input_mininfo"),
434         // MessageManager.getString("info.invalid_msa_notenough"),
435         // JvOptionPane.INFORMATION_MESSAGE);
436       }
437     }
438
439     @Override
440     public boolean pollJobs()
441     {
442       boolean done = true;
443       for (WSJob job : getJobs())
444       {
445         if (!job.getStatus().isDone())
446         {
447           Cache.log.debug(format("Polling job %s.", job));
448           try
449           {
450             service.updateProgress(job);
451             exceptionCount.remove(job.getUid());
452           } catch (IOException e)
453           {
454             Cache.log.error(format("Polling job %s failed.", job), e);
455             wsInfo.appendProgressText(job.getJobNum(),
456                     MessageManager.formatMessage("info.server_exception",
457                             service.getName(), e.getMessage()));
458             int count = exceptionCount.getOrDefault(job.getUid(),
459                     MAX_RETRY);
460             if (--count <= 0)
461             {
462               job.setStatus(WSJobStatus.SERVER_ERROR);
463               Cache.log.warn(format(
464                       "Attempts limit exceeded. Droping job %s.", job));
465             }
466             exceptionCount.put(job.getUid(), count);
467           } catch (OutOfMemoryError e)
468           {
469             job.setStatus(WSJobStatus.BROKEN);
470             Cache.log.error(
471                     format("Out of memory when retrieving job %s", job), e);
472           }
473           Cache.log.debug(
474                   format("Job %s status is %s", job, job.getStatus()));
475         }
476         done &= job.getStatus().isDone();
477       }
478       updateWSInfoGlobalStatus();
479       return done;
480     }
481
482     private void updateWSInfoGlobalStatus()
483     {
484       if (jobs.countRunning() > 0)
485       {
486         wsInfo.setStatus(WebserviceInfo.STATE_RUNNING);
487       }
488       else if (jobs.countQueuing() > 0
489               || jobs.countSubmitted() < jobs.size())
490       {
491         wsInfo.setStatus(WebserviceInfo.STATE_QUEUING);
492       }
493       else
494       {
495         if (jobs.countSuccessful() > 0)
496         {
497           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK);
498         }
499         else if (jobs.countCancelled() > 0)
500         {
501           wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK);
502         }
503         else if (jobs.countFailed() > 0)
504         {
505           wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
506         }
507       }
508     }
509
510     @Override
511     public void done()
512     {
513       long progbarId = MathUtils.getUID();
514       wsInfo.setProgressBar(
515               MessageManager.getString("status.collecting_job_results"),
516               progbarId);
517       Map<Long, AlignmentI> results = new LinkedHashMap<>();
518       for (WSJob job : getJobs())
519       {
520         try
521         {
522           AlignmentI alignment = supplier.getResult(job);
523           if (alignment != null)
524           {
525             results.put(job.getUid(), alignment);
526           }
527         } catch (Exception e)
528         {
529           if (!service.handleCollectionError(job, e))
530           {
531             Cache.log.error("Couldn't get alignment for job.", e);
532             // TODO: Increment exception count and retry.
533             job.setStatus(WSJobStatus.SERVER_ERROR);
534           }
535         }
536       }
537       updateWSInfoGlobalStatus();
538       if (results.size() > 0)
539       {
540         OutputWrapper out = prepareOutput(results);
541         wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame(
542                 new Alignment(out.aln), out.alorders, out.hidden));
543         wsInfo.setResultsReady();
544       }
545       else
546       {
547         wsInfo.setFinishedNoResults();
548       }
549       wsInfo.removeProgressBar(progbarId);
550     }
551
552     private class OutputWrapper
553     {
554       AlignmentI aln;
555
556       List<AlignmentOrder> alorders;
557
558       HiddenColumns hidden;
559
560       OutputWrapper(AlignmentI aln, List<AlignmentOrder> alorders,
561               HiddenColumns hidden)
562       {
563         this.aln = aln;
564         this.alorders = alorders;
565         this.hidden = hidden;
566       }
567     }
568
569     private OutputWrapper prepareOutput(Map<Long, AlignmentI> alignments)
570     {
571       List<AlignmentOrder> alorders = new ArrayList<>();
572       SequenceI[][] results = new SequenceI[jobs.size()][];
573       AlignmentOrder[] orders = new AlignmentOrder[jobs.size()];
574       for (int i = 0; i < jobs.size(); i++)
575       {
576         WSJob job = jobs.get(i);
577         AlignmentI aln = alignments.get(job.getUid());
578         if (aln != null) // equivalent of job.hasResults()
579         {
580           /* Get the alignment including any empty sequences in the original
581            * order with original ids. */
582           JobInput input = inputs.get(job.getUid());
583           char gapChar = aln.getGapCharacter();
584           List<SequenceI> emptySeqs = input.emptySequences;
585           List<SequenceI> alnSeqs = aln.getSequences();
586           // find the width of the longest sequence
587           int width = 0;
588           for (var seq : alnSeqs)
589             width = Integer.max(width, seq.getLength());
590           for (var emptySeq : emptySeqs)
591             width = Integer.max(width, emptySeq.getLength());
592           // pad shorter sequences with gaps
593           String gapSeq = String.join("",
594                   Collections.nCopies(width, Character.toString(gapChar)));
595           List<SequenceI> seqs = new ArrayList<>(
596                   alnSeqs.size() + emptySeqs.size());
597           seqs.addAll(alnSeqs);
598           seqs.addAll(emptySeqs);
599           for (var seq : seqs)
600           {
601             if (seq.getLength() < width)
602               seq.setSequence(seq.getSequenceAsString()
603                       + gapSeq.substring(seq.getLength()));
604           }
605           SequenceI[] result = seqs.toArray(new SequenceI[0]);
606           AlignmentOrder msaOrder = new AlignmentOrder(result);
607           AlignmentSorter.recoverOrder(result);
608           // temporary workaround for deuniquify
609           @SuppressWarnings({ "rawtypes", "unchecked" })
610           Hashtable names = new Hashtable(input.sequenceNames);
611           // FIXME first call to deuniquify alters original alignment
612           SeqsetUtils.deuniquify(names, result);
613           alorders.add(msaOrder);
614           results[i] = result;
615           orders[i] = msaOrder;
616         }
617         else
618         {
619           results[i] = null;
620         }
621       }
622
623       Object[] newView = msa.getUpdatedView(results, orders, gapCharacter);
624       // free references to original data
625       for (int i = 0; i < jobs.size(); i++)
626       {
627         results[i] = null;
628         orders[i] = null;
629       }
630       SequenceI[] alignment = (SequenceI[]) newView[0];
631       HiddenColumns hidden = (HiddenColumns) newView[1];
632       Alignment aln = new Alignment(alignment);
633       aln.setProperty("Alignment Program", service.getName());
634       if (dataset != null)
635         aln.setDataset(dataset);
636
637       propagateDatasetMappings(aln);
638       return new OutputWrapper(aln, alorders, hidden);
639       // displayNewFrame(aln, alorders, hidden);
640     }
641
642     /*
643      * conserves dataset references to sequence objects returned from web
644      * services. propagate codon frame data to alignment.
645      */
646     private void propagateDatasetMappings(Alignment aln)
647     {
648       if (codonFrame != null)
649       {
650         SequenceI[] alignment = aln.getSequencesArray();
651         for (SequenceI seq : alignment)
652         {
653           for (AlignedCodonFrame acf : codonFrame)
654           {
655             if (acf != null && acf.involvesSequence(seq))
656             {
657               aln.addCodonFrame(acf);
658               break;
659             }
660           }
661         }
662       }
663     }
664
665     private void displayNewFrame(AlignmentI aln,
666             List<AlignmentOrder> alorders, HiddenColumns hidden)
667     {
668       AlignFrame frame = new AlignFrame(aln, hidden,
669               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
670       // TODO store feature renderer settings in worker object
671       // frame.getFeatureRenderer().transferSettings(featureSettings);
672       var regions = sortOrders(alorders);
673       if (alorders.size() == 1)
674       {
675         frame.addSortByOrderMenuItem(
676                 format("%s Ordering", service.getName()), alorders.get(0));
677       }
678       else
679       {
680         for (int i = 0; i < alorders.size(); i++)
681         {
682           final int j = i;
683           Iterable<String> iter = () -> regions.get(j).stream()
684                   .map(it -> Integer.toString(it)).iterator();
685           var orderName = format("%s Region %s Ordering", service.getName(),
686                   String.join(",", iter));
687           frame.addSortByOrderMenuItem(orderName, alorders.get(i));
688         }
689       }
690
691       /* TODO
692        * If alignment was requested from one half of a SplitFrame, show in a
693        * SplitFrame with the other pane similarly aligned.
694        */
695
696       Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH,
697               AlignFrame.DEFAULT_HEIGHT);
698     }
699
700     private List<List<Integer>> sortOrders(List<?> alorders)
701     {
702       List<List<Integer>> regions = new ArrayList<>();
703       for (int i = 0; i < alorders.size(); i++)
704       {
705         List<Integer> regs = new ArrayList<>();
706         regs.add(i);
707         int j = i + 1;
708         while (j < alorders.size())
709         {
710           if (alorders.get(i).equals(alorders.get(j)))
711           {
712             alorders.remove(j);
713             regs.add(j);
714           }
715           else
716           {
717             j++;
718           }
719         }
720         regions.add(regs);
721       }
722       return regions;
723     }
724   }
725
726   private static class JobInput
727   {
728     final List<SequenceI> inputSequences;
729
730     final List<SequenceI> emptySequences;
731
732     @SuppressWarnings("rawtypes")
733     final Map<String, ? extends Map> sequenceNames;
734
735     private JobInput(int numSequences, List<SequenceI> inputSequences,
736             List<SequenceI> emptySequences,
737             @SuppressWarnings("rawtypes") Map<String, ? extends Map> names)
738     {
739       this.inputSequences = Collections.unmodifiableList(inputSequences);
740       this.emptySequences = Collections.unmodifiableList(emptySequences);
741       this.sequenceNames = names;
742     }
743
744     boolean isInputValid()
745     {
746       return inputSequences.size() >= 2;
747     }
748
749     static JobInput create(SequenceI[] sequences, int minLength,
750             boolean submitGaps)
751     {
752       assert minLength >= 0 : MessageManager.getString(
753               "error.implementation_error_minlen_must_be_greater_zero");
754       int numSeq = 0;
755       for (SequenceI seq : sequences)
756       {
757         if (seq.getEnd() - seq.getStart() >= minLength)
758         {
759           numSeq++;
760         }
761       }
762
763       List<SequenceI> inputSequences = new ArrayList<>();
764       List<SequenceI> emptySequences = new ArrayList<>();
765       @SuppressWarnings("rawtypes")
766       Map<String, Hashtable> names = new LinkedHashMap<>();
767       for (int i = 0; i < sequences.length; i++)
768       {
769         SequenceI seq = sequences[i];
770         String newName = SeqsetUtils.unique_name(i);
771         @SuppressWarnings("rawtypes")
772         Hashtable hash = SeqsetUtils.SeqCharacterHash(seq);
773         names.put(newName, hash);
774         if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength)
775         {
776           String seqString = seq.getSequenceAsString();
777           if (!submitGaps)
778           {
779             seqString = AlignSeq.extractGaps(
780                     jalview.util.Comparison.GapChars, seqString);
781           }
782           inputSequences.add(new Sequence(newName, seqString));
783         }
784         else
785         {
786           String seqString = null;
787           if (seq.getEnd() >= seq.getStart()) // is it ever false?
788           {
789             seqString = seq.getSequenceAsString();
790             if (!submitGaps)
791             {
792               seqString = AlignSeq.extractGaps(
793                       jalview.util.Comparison.GapChars, seqString);
794             }
795           }
796           emptySequences.add(new Sequence(newName, seqString));
797         }
798       }
799
800       return new JobInput(numSeq, inputSequences, emptySequences, names);
801     }
802   }
803
804 }