1 /* 2 * Jalview - A Sequence Alignment Editor and Viewer (2.11.1.4) 3 * Copyright (C) 2021 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.jws1; 22 23 import jalview.analysis.AlignSeq; 24 import jalview.api.FeatureColourI; 25 import jalview.bin.Cache; 26 import jalview.datamodel.Alignment; 27 import jalview.datamodel.AlignmentI; 28 import jalview.datamodel.AlignmentView; 29 import jalview.datamodel.SequenceI; 30 import jalview.gui.AlignFrame; 31 import jalview.gui.Desktop; 32 import jalview.gui.WebserviceInfo; 33 import jalview.io.NewickFile; 34 import jalview.util.MessageManager; 35 import jalview.ws.AWsJob; 36 import jalview.ws.JobStateSummary; 37 import jalview.ws.WSClientI; 38 39 import java.util.HashMap; 40 import java.util.Hashtable; 41 import java.util.Map; 42 import java.util.Vector; 43 44 import vamsas.objects.simple.MsaResult; 45 import vamsas.objects.simple.SeqSearchResult; 46 47 class SeqSearchWSThread extends JWS1Thread implements WSClientI 48 { 49 String dbs = null; 50 51 boolean profile = false; 52 53 class SeqSearchWSJob extends WSJob 54 { 55 // hold special input for this 56 vamsas.objects.simple.SequenceSet seqs = new vamsas.objects.simple.SequenceSet(); 57 58 /** 59 * MsaWSJob 60 * 61 * @param jobNum 62 * int 63 * @param jobId 64 * String 65 */ SeqSearchWSJob(int jobNum, SequenceI[] inSeqs)66 public SeqSearchWSJob(int jobNum, SequenceI[] inSeqs) 67 { 68 this.jobnum = jobNum; 69 if (!prepareInput(inSeqs, 2)) 70 { 71 submitted = true; 72 subjobComplete = true; 73 result = new MsaResult(); 74 result.setFinished(true); 75 result.setStatus(MessageManager.getString("label.job_never_ran")); 76 } 77 78 } 79 80 Hashtable SeqNames = new Hashtable(); 81 82 Vector emptySeqs = new Vector(); 83 84 /** 85 * prepare input sequences for service 86 * 87 * @param seqs 88 * jalview sequences to be prepared 89 * @param minlen 90 * minimum number of residues required for this MsaWS service 91 * @return true if seqs contains sequences to be submitted to service. 92 */ prepareInput(SequenceI[] seqs, int minlen)93 private boolean prepareInput(SequenceI[] seqs, int minlen) 94 { 95 int nseqs = 0; 96 if (minlen < 0) 97 { 98 throw new Error(MessageManager.getString( 99 "error.implementation_error_minlen_must_be_greater_zero")); 100 } 101 for (int i = 0; i < seqs.length; i++) 102 { 103 if (seqs[i].getEnd() - seqs[i].getStart() > minlen - 1) 104 { 105 nseqs++; 106 } 107 } 108 boolean valid = nseqs >= 1; // need at least one sequence for valid input 109 // TODO: generalise 110 vamsas.objects.simple.Sequence[] seqarray = (valid) 111 ? new vamsas.objects.simple.Sequence[nseqs] 112 : null; 113 boolean submitGaps = (nseqs == 1) ? false : true; // profile is submitted 114 // with gaps 115 for (int i = 0, n = 0; i < seqs.length; i++) 116 { 117 118 String newname = jalview.analysis.SeqsetUtils.unique_name(i); // same 119 // for 120 // any 121 // subjob 122 SeqNames.put(newname, 123 jalview.analysis.SeqsetUtils.SeqCharacterHash(seqs[i])); 124 if (valid && seqs[i].getEnd() - seqs[i].getStart() > minlen - 1) 125 { 126 seqarray[n] = new vamsas.objects.simple.Sequence(); 127 seqarray[n].setId(newname); 128 seqarray[n++].setSeq((submitGaps) ? seqs[i].getSequenceAsString() 129 : AlignSeq.extractGaps(jalview.util.Comparison.GapChars, 130 seqs[i].getSequenceAsString())); 131 } 132 else 133 { 134 String empty = null; 135 if (seqs[i].getEnd() >= seqs[i].getStart()) 136 { 137 empty = (submitGaps) ? seqs[i].getSequenceAsString() 138 : AlignSeq.extractGaps(jalview.util.Comparison.GapChars, 139 seqs[i].getSequenceAsString()); 140 } 141 emptySeqs.add(new String[] { newname, empty }); 142 } 143 } 144 if (submitGaps) 145 { 146 // almost certainly have to remove gapped columns here 147 } 148 this.seqs = new vamsas.objects.simple.SequenceSet(); 149 this.seqs.setSeqs(seqarray); 150 return valid; 151 } 152 153 /** 154 * 155 * @return true if getAlignment will return a valid alignment result. 156 */ 157 @Override hasResults()158 public boolean hasResults() 159 { 160 if (subjobComplete && result != null && result.isFinished() 161 && ((SeqSearchResult) result).getAlignment() != null 162 && ((SeqSearchResult) result).getAlignment() 163 .getSeqs() != null) 164 { 165 return true; 166 } 167 return false; 168 } 169 170 /** 171 * return sequence search results for display 172 * 173 * @return null or { Alignment(+features and annotation), NewickFile)} 174 */ getAlignment(AlignmentI dataset, Map<String, FeatureColourI> featureColours)175 public Object[] getAlignment(AlignmentI dataset, 176 Map<String, FeatureColourI> featureColours) 177 { 178 179 if (result != null && result.isFinished()) 180 { 181 SequenceI[] alseqs = null; 182 // char alseq_gapchar = '-'; 183 // int alseq_l = 0; 184 if (((SeqSearchResult) result).getAlignment() != null) 185 { 186 alseqs = getVamsasAlignment( 187 ((SeqSearchResult) result).getAlignment()); 188 // alseq_gapchar = ( (SeqSearchResult) 189 // result).getAlignment().getGapchar().charAt(0); 190 // alseq_l = alseqs.length; 191 } 192 /** 193 * what has to be done. 1 - annotate returned alignment with annotation 194 * file and sequence features file, and associate any tree-nodes. 2. 195 * connect alignment back to any associated dataset: 2.a. deuniquify 196 * recovers sequence information - but additionally, relocations must be 197 * made from the returned aligned sequence back to the dataset. 198 */ 199 // construct annotated alignment as it would be done by the jalview 200 // applet 201 jalview.datamodel.Alignment al = new Alignment(alseqs); 202 // al.setDataset(dataset); 203 // make dataset 204 String inFile = null; 205 try 206 { 207 inFile = ((SeqSearchResult) result).getAnnotation(); 208 if (inFile != null && inFile.length() > 0) 209 { 210 new jalview.io.AnnotationFile().readAnnotationFile(al, inFile, 211 jalview.io.DataSourceType.PASTE); 212 } 213 } catch (Exception e) 214 { 215 System.err.println( 216 "Failed to parse the annotation file associated with the alignment."); 217 System.err.println(">>>EOF" + inFile + "\n<<<EOF\n"); 218 e.printStackTrace(System.err); 219 } 220 221 try 222 { 223 inFile = ((SeqSearchResult) result).getFeatures(); 224 if (inFile != null && inFile.length() > 0) 225 { 226 jalview.io.FeaturesFile ff = new jalview.io.FeaturesFile(inFile, 227 jalview.io.DataSourceType.PASTE); 228 ff.parse(al, featureColours, false); 229 } 230 } catch (Exception e) 231 { 232 System.err.println( 233 "Failed to parse the Features file associated with the alignment."); 234 System.err.println(">>>EOF" + inFile + "\n<<<EOF\n"); 235 e.printStackTrace(System.err); 236 } 237 jalview.io.NewickFile nf = null; 238 try 239 { 240 inFile = ((SeqSearchResult) result).getNewickTree(); 241 if (inFile != null && inFile.length() > 0) 242 { 243 nf = new jalview.io.NewickFile(inFile, 244 jalview.io.DataSourceType.PASTE); 245 if (!nf.isValid()) 246 { 247 nf.close(); 248 nf = null; 249 } 250 } 251 } catch (Exception e) 252 { 253 System.err.println( 254 "Failed to parse the treeFile associated with the alignment."); 255 System.err.println(">>>EOF" + inFile + "\n<<<EOF\n"); 256 e.printStackTrace(System.err); 257 } 258 259 /* 260 * TODO: housekeeping w.r.t. recovery of dataset and annotation 261 * references for input sequences, and then dataset sequence creation 262 * for new sequences retrieved from service // finally, attempt to 263 * de-uniquify to recover input sequence identity, and try to map back 264 * onto dataset Note: this 265 * jalview.analysis.SeqsetUtils.deuniquify(SeqNames, alseqs, true); will 266 * NOT WORK - the returned alignment may contain multiple versions of 267 * the input sequence, each being a subsequence of the original. 268 * deuniquify also removes existing annotation and features added in the 269 * previous step... al.setDataset(dataset); // add in new sequences 270 * retrieved from sequence search which are not already in dataset. // 271 * trigger a 'fetchDBids' to annotate sequences with database ids... 272 */ 273 274 return new Object[] { al, nf }; 275 } 276 return null; 277 } 278 279 /** 280 * mark subjob as cancelled and set result object appropriatly 281 */ cancel()282 void cancel() 283 { 284 cancelled = true; 285 subjobComplete = true; 286 result = null; 287 } 288 289 /** 290 * 291 * @return boolean true if job can be submitted. 292 */ 293 @Override hasValidInput()294 public boolean hasValidInput() 295 { 296 if (seqs.getSeqs() != null) 297 { 298 return true; 299 } 300 return false; 301 } 302 } 303 304 String alTitle; // name which will be used to form new alignment window. 305 306 AlignmentI dataset; // dataset to which the new alignment will be 307 308 // associated. 309 310 ext.vamsas.SeqSearchI server = null; 311 312 private String dbArg; 313 314 /** 315 * set basic options for this (group) of Msa jobs 316 * 317 * @param subgaps 318 * boolean 319 * @param presorder 320 * boolean 321 */ SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl, WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame, AlignmentView alview, String wsname, String db)322 SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl, 323 WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame, 324 AlignmentView alview, String wsname, String db) 325 { 326 super(alFrame, wsinfo, alview, wsname, wsUrl); 327 this.server = server; 328 this.dbArg = db; 329 } 330 331 /** 332 * create one or more Msa jobs to align visible seuqences in _msa 333 * 334 * @param title 335 * String 336 * @param _msa 337 * AlignmentView 338 * @param subgaps 339 * boolean 340 * @param presorder 341 * boolean 342 * @param seqset 343 * Alignment 344 */ SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl, WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame, String wsname, String title, AlignmentView _msa, String db, AlignmentI seqset)345 SeqSearchWSThread(ext.vamsas.SeqSearchI server, String wsUrl, 346 WebserviceInfo wsinfo, jalview.gui.AlignFrame alFrame, 347 String wsname, String title, AlignmentView _msa, String db, 348 AlignmentI seqset) 349 { 350 this(server, wsUrl, wsinfo, alFrame, _msa, wsname, db); 351 OutputHeader = wsInfo.getProgressText(); 352 alTitle = title; 353 dataset = seqset; 354 355 SequenceI[][] conmsa = _msa.getVisibleContigs('-'); 356 if (conmsa != null) 357 { 358 int njobs = conmsa.length; 359 jobs = new SeqSearchWSJob[njobs]; 360 for (int j = 0; j < njobs; j++) 361 { 362 if (j != 0) 363 { 364 jobs[j] = new SeqSearchWSJob(wsinfo.addJobPane(), conmsa[j]); 365 } 366 else 367 { 368 jobs[j] = new SeqSearchWSJob(0, conmsa[j]); 369 } 370 if (njobs > 0) 371 { 372 wsinfo.setProgressName("region " + jobs[j].getJobnum(), 373 jobs[j].getJobnum()); 374 } 375 wsinfo.setProgressText(jobs[j].getJobnum(), OutputHeader); 376 } 377 } 378 } 379 380 @Override isCancellable()381 public boolean isCancellable() 382 { 383 return true; 384 } 385 386 @Override cancelJob()387 public void cancelJob() 388 { 389 if (!jobComplete && jobs != null) 390 { 391 boolean cancelled = true; 392 for (int job = 0; job < jobs.length; job++) 393 { 394 if (jobs[job].isSubmitted() && !jobs[job].isSubjobComplete()) 395 { 396 String cancelledMessage = ""; 397 try 398 { 399 vamsas.objects.simple.WsJobId cancelledJob = server 400 .cancel(jobs[job].getJobId()); 401 if (cancelledJob.getStatus() == 2) 402 { 403 // CANCELLED_JOB 404 cancelledMessage = "Job cancelled."; 405 ((SeqSearchWSJob) jobs[job]).cancel(); 406 wsInfo.setStatus(jobs[job].getJobnum(), 407 WebserviceInfo.STATE_CANCELLED_OK); 408 } 409 else if (cancelledJob.getStatus() == 3) 410 { 411 // VALID UNSTOPPABLE JOB 412 cancelledMessage += "Server cannot cancel this job. just close the window.\n"; 413 cancelled = false; 414 // wsInfo.setStatus(jobs[job].jobnum, 415 // WebserviceInfo.STATE_RUNNING); 416 } 417 418 if (cancelledJob.getJobId() != null) 419 { 420 cancelledMessage += ("[" + cancelledJob.getJobId() + "]"); 421 } 422 423 cancelledMessage += "\n"; 424 } catch (Exception exc) 425 { 426 cancelledMessage += ("\nProblems cancelling the job : Exception received...\n" 427 + exc + "\n"); 428 Cache.log.warn( 429 "Exception whilst cancelling " + jobs[job].getJobId(), 430 exc); 431 } 432 wsInfo.setProgressText(jobs[job].getJobnum(), 433 OutputHeader + cancelledMessage + "\n"); 434 } 435 } 436 if (cancelled) 437 { 438 wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK); 439 jobComplete = true; 440 } 441 this.interrupt(); // kick thread to update job states. 442 } 443 else 444 { 445 if (!jobComplete) 446 { 447 wsInfo.setProgressText(OutputHeader 448 + "Server cannot cancel this job because it has not been submitted properly. just close the window.\n"); 449 } 450 } 451 } 452 453 @Override pollJob(AWsJob job)454 public void pollJob(AWsJob job) throws Exception 455 { 456 ((SeqSearchWSJob) job).result = server 457 .getResult(((SeqSearchWSJob) job).getJobId()); 458 } 459 460 @Override StartJob(AWsJob job)461 public void StartJob(AWsJob job) 462 { 463 if (!(job instanceof SeqSearchWSJob)) 464 { 465 throw new Error(MessageManager.formatMessage( 466 "error.implementation_error_msawbjob_called", new String[] 467 { job.getClass().toString() })); 468 } 469 SeqSearchWSJob j = (SeqSearchWSJob) job; 470 if (j.isSubmitted()) 471 { 472 if (Cache.log.isDebugEnabled()) 473 { 474 Cache.log.debug( 475 "Tried to submit an already submitted job " + j.getJobId()); 476 } 477 return; 478 } 479 if (j.seqs.getSeqs() == null) 480 { 481 // special case - selection consisted entirely of empty sequences... 482 j.setSubmitted(true); 483 j.result = new MsaResult(); 484 j.result.setFinished(true); 485 j.result.setStatus( 486 MessageManager.getString("label.empty_alignment_job")); 487 ((MsaResult) j.result).setMsa(null); 488 } 489 try 490 { 491 vamsas.objects.simple.WsJobId jobsubmit = server 492 .search(j.seqs.getSeqs()[0], dbArg); 493 494 if ((jobsubmit != null) && (jobsubmit.getStatus() == 1)) 495 { 496 j.setJobId(jobsubmit.getJobId()); 497 j.setSubmitted(true); 498 j.setSubjobComplete(false); 499 // System.out.println(WsURL + " Job Id '" + jobId + "'"); 500 } 501 else 502 { 503 if (jobsubmit == null) 504 { 505 throw new Exception(MessageManager.formatMessage( 506 "exception.web_service_returned_null_try_later", 507 new String[] 508 { WsUrl })); 509 } 510 511 throw new Exception(jobsubmit.getJobId()); 512 } 513 } catch (Exception e) 514 { 515 // TODO: JBPNote catch timeout or other fault types explicitly 516 // For unexpected errors 517 System.err.println(WebServiceName 518 + "Client: Failed to submit the sequences for alignment (probably a server side problem)\n" 519 + "When contacting Server:" + WsUrl + "\n" + e.toString() 520 + "\n"); 521 j.setAllowedServerExceptions(0); 522 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR); 523 wsInfo.setStatus(j.getJobnum(), 524 WebserviceInfo.STATE_STOPPED_SERVERERROR); 525 wsInfo.appendProgressText(j.getJobnum(), MessageManager 526 .getString("info.failed_to_submit_sequences_for_alignment")); 527 528 // e.printStackTrace(); // TODO: JBPNote DEBUG 529 } 530 } 531 getVamsasAlignment( vamsas.objects.simple.Alignment valign)532 private jalview.datamodel.Sequence[] getVamsasAlignment( 533 vamsas.objects.simple.Alignment valign) 534 { 535 vamsas.objects.simple.Sequence[] seqs = valign.getSeqs().getSeqs(); 536 jalview.datamodel.Sequence[] msa = new jalview.datamodel.Sequence[seqs.length]; 537 538 for (int i = 0, j = seqs.length; i < j; i++) 539 { 540 msa[i] = new jalview.datamodel.Sequence(seqs[i].getId(), 541 seqs[i].getSeq()); 542 } 543 544 return msa; 545 } 546 547 @Override parseResult()548 public void parseResult() 549 { 550 int results = 0; // number of result sets received 551 JobStateSummary finalState = new JobStateSummary(); 552 try 553 { 554 for (int j = 0; j < jobs.length; j++) 555 { 556 finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]); 557 if (jobs[j].isSubmitted() && jobs[j].isSubjobComplete() 558 && jobs[j].hasResults()) 559 { 560 results++; 561 vamsas.objects.simple.Alignment valign = ((SeqSearchResult) ((SeqSearchWSJob) jobs[j]).result) 562 .getAlignment(); 563 if (valign != null) 564 { 565 wsInfo.appendProgressText(jobs[j].getJobnum(), MessageManager 566 .getString("info.alignment_object_method_notes")); 567 String[] lines = valign.getMethod(); 568 for (int line = 0; line < lines.length; line++) 569 { 570 wsInfo.appendProgressText(jobs[j].getJobnum(), 571 lines[line] + "\n"); 572 } 573 // JBPNote The returned files from a webservice could be 574 // hidden behind icons in the monitor window that, 575 // when clicked, pop up their corresponding data 576 } 577 } 578 } 579 } catch (Exception ex) 580 { 581 582 Cache.log.error( 583 "Unexpected exception when processing results for " + alTitle, 584 ex); 585 wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR); 586 } 587 if (results > 0) 588 { 589 wsInfo.showResultsNewFrame 590 .addActionListener(new java.awt.event.ActionListener() 591 { 592 @Override 593 public void actionPerformed(java.awt.event.ActionEvent evt) 594 { 595 displayResults(true); 596 } 597 }); 598 wsInfo.mergeResults 599 .addActionListener(new java.awt.event.ActionListener() 600 { 601 @Override 602 public void actionPerformed(java.awt.event.ActionEvent evt) 603 { 604 displayResults(false); 605 } 606 }); 607 wsInfo.setResultsReady(); 608 } 609 else 610 { 611 wsInfo.setFinishedNoResults(); 612 } 613 } 614 displayResults(boolean newFrame)615 void displayResults(boolean newFrame) 616 { 617 if (!newFrame) 618 { 619 System.err.println("MERGE WITH OLD FRAME NOT IMPLEMENTED"); 620 return; 621 } 622 // each subjob is an independent alignment for the moment 623 // Alignment al[] = new Alignment[jobs.length]; 624 // NewickFile nf[] = new NewickFile[jobs.length]; 625 for (int j = 0; j < jobs.length; j++) 626 { 627 Map<String, FeatureColourI> featureColours = new HashMap<String, FeatureColourI>(); 628 Alignment al = null; 629 NewickFile nf = null; 630 if (jobs[j].hasResults()) 631 { 632 Object[] res = ((SeqSearchWSJob) jobs[j]).getAlignment(dataset, 633 featureColours); 634 if (res == null) 635 { 636 continue; 637 } 638 ; 639 al = (Alignment) res[0]; 640 nf = (NewickFile) res[1]; 641 } 642 else 643 { 644 al = null; 645 nf = null; 646 continue; 647 } 648 /* 649 * We can't map new alignment back with insertions from input's hidden 650 * regions until dataset mapping is sorted out... but basically it goes 651 * like this: 1. Merge each domain hit back onto the visible segments in 652 * the same way as a Jnet prediction is mapped back 653 * 654 * Object[] newview = input.getUpdatedView(results, orders, getGapChar()); 655 * // trash references to original result data for (int j = 0; j < 656 * jobs.length; j++) { results[j] = null; orders[j] = null; } SequenceI[] 657 * alignment = (SequenceI[]) newview[0]; ColumnSelection columnselection = 658 * (ColumnSelection) newview[1]; Alignment al = new Alignment(alignment); 659 * 660 * if (dataset != null) { al.setDataset(dataset); } 661 * 662 * propagateDatasetMappings(al); } 663 */ 664 665 AlignFrame af = new AlignFrame(al, // columnselection, 666 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); 667 if (nf != null) 668 { 669 af.showNewickTree(nf, 670 MessageManager.formatMessage("label.tree_from", new String[] 671 { this.alTitle })); 672 } 673 // initialise with same renderer settings as in parent alignframe. 674 af.getFeatureRenderer().transferSettings(this.featureSettings); 675 Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH, 676 AlignFrame.DEFAULT_HEIGHT); 677 } 678 } 679 680 @Override canMergeResults()681 public boolean canMergeResults() 682 { 683 return false; 684 } 685 } 686