/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ The Jalview Authors
*
* This file is part of Jalview.
*
* Jalview is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Jalview is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Jalview. If not, see .
* The Jalview Authors are detailed in the 'AUTHORS' file.
*/
package jalview.ws.jws2;
import jalview.analysis.AlignSeq;
import jalview.analysis.SeqsetUtils;
import jalview.api.AlignViewportI;
import jalview.api.AlignmentViewPanel;
import jalview.bin.Cache;
import jalview.datamodel.AlignmentAnnotation;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.AnnotatedCollectionI;
import jalview.datamodel.SequenceI;
import jalview.gui.AlignFrame;
import jalview.gui.Desktop;
import jalview.gui.IProgressIndicator;
import jalview.gui.IProgressIndicatorHandler;
import jalview.gui.JvOptionPane;
import jalview.schemes.ResidueProperties;
import jalview.util.MessageManager;
import jalview.workers.AlignCalcWorker;
import jalview.ws.api.CancellableI;
import jalview.ws.api.JobId;
import jalview.ws.api.SequenceAnnotationServiceI;
import jalview.ws.api.WSAnnotationCalcManagerI;
import jalview.ws.gui.AnnotationWsJob;
import jalview.ws.jws2.dm.AAConSettings;
import jalview.ws.jws2.jabaws2.Jws2Instance;
import jalview.ws.params.ArgumentI;
import jalview.ws.params.WsParamSetI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AbstractJabaCalcWorker extends AlignCalcWorker
implements WSAnnotationCalcManagerI
{
protected Jws2Instance service;
protected WsParamSetI preset;
protected List arguments;
protected IProgressIndicator guiProgress;
protected boolean submitGaps = true;
/**
* by default, we filter out non-standard residues before submission
*/
protected boolean filterNonStandardResidues = true;
/**
* Recover any existing parameters for this service
*/
protected void initViewportParams()
{
if (getCalcId() != null)
{
((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
getCalcId(),
new AAConSettings(true, service, this.preset, arguments),
true);
}
}
/**
*
* @return null or a string used to recover all annotation generated by this
* worker
*/
public String getCalcId()
{
return service.getAlignAnalysisUI() == null ? null
: service.getAlignAnalysisUI().getCalcId();
}
public WsParamSetI getPreset()
{
return preset;
}
public List getArguments()
{
return arguments;
}
/**
* reconfigure and restart the AAConClient. This method will spawn a new
* thread that will wait until any current jobs are finished, modify the
* parameters and restart the conservation calculation with the new values.
*
* @param newpreset
* @param newarguments
*/
public void updateParameters(final WsParamSetI newpreset,
final List newarguments)
{
preset = newpreset;
arguments = newarguments;
calcMan.startWorker(this);
initViewportParams();
}
protected boolean alignedSeqs = true;
protected boolean nucleotidesAllowed = false;
protected boolean proteinAllowed = false;
/**
* record sequences for mapping result back to afterwards
*/
protected boolean bySequence = false;
protected Map seqNames;
// TODO: convert to bitset
protected boolean[] gapMap;
int realw;
protected int start;
int end;
private AlignFrame alignFrame;
public boolean[] getGapMap()
{
return gapMap;
}
public AbstractJabaCalcWorker(AlignViewportI alignViewport,
AlignmentViewPanel alignPanel)
{
super(alignViewport, alignPanel);
}
public AbstractJabaCalcWorker(Jws2Instance service, AlignFrame alignFrame,
WsParamSetI preset, List paramset)
{
this(alignFrame.getCurrentView(), alignFrame.alignPanel);
// TODO: both these fields needed ?
this.alignFrame = alignFrame;
this.guiProgress = alignFrame;
this.preset = preset;
this.arguments = paramset;
this.service = service;
try
{
annotService = (jalview.ws.api.SequenceAnnotationServiceI) service
.getEndpoint();
} catch (ClassCastException cce)
{
JvOptionPane.showMessageDialog(Desktop.desktop,
MessageManager.formatMessage(
"label.service_called_is_not_an_annotation_service",
new String[]
{ service.getName() }),
MessageManager.getString("label.internal_jalview_error"),
JvOptionPane.WARNING_MESSAGE);
}
// configure submission flags
if (service.getAlignAnalysisUI() != null)
{
// instantaneous calculation. Right now that's either AACons or RNAAliFold
proteinAllowed = service.getAlignAnalysisUI().isPr();
nucleotidesAllowed = service.getAlignAnalysisUI().isNa();
alignedSeqs = service.getAlignAnalysisUI().isNeedsAlignedSeqs();
bySequence = !service.getAlignAnalysisUI().isAA();
filterNonStandardResidues = service.getAlignAnalysisUI()
.isFilterSymbols();
min_valid_seqs = service.getAlignAnalysisUI().getMinimumSequences();
initViewportParams();
}
else
{
// assume disorder prediction : per-sequence protein only no gaps
// analysis.
// TODO - move configuration to UIInfo base class for all these flags !
alignedSeqs = false;
bySequence = true;
filterNonStandardResidues = true;
nucleotidesAllowed = false;
proteinAllowed = true;
submitGaps = false;
min_valid_seqs = 1;
}
}
/**
*
* @return true if the submission thread should attempt to submit data
*/
public boolean hasService()
{
return annotService != null;
}
protected jalview.ws.api.SequenceAnnotationServiceI annotService = null;
volatile JobId rslt = null;
AnnotationWsJob running = null;
private int min_valid_seqs;
@Override
public void run()
{
if (!hasService())
{
return;
}
long progressId = -1;
int serverErrorsLeft = 3;
final boolean cancellable = CancellableI.class
.isAssignableFrom(annotService.getClass());
StringBuffer msg = new StringBuffer();
try
{
if (checkDone())
{
return;
}
List seqs = getInputSequences(
alignViewport.getAlignment(),
bySequence ? alignViewport.getSelectionGroup() : null);
if (seqs == null || !checkValidInputSeqs(seqs))
{
jalview.bin.Cache.log.debug(
"Sequences for analysis service were null or not valid");
calcMan.workerComplete(this);
return;
}
AlignmentAnnotation[] aa = alignViewport.getAlignment()
.getAlignmentAnnotation();
if (guiProgress != null)
{
guiProgress.setProgressBar(service.getActionText(),
progressId = System.currentTimeMillis());
}
jalview.bin.Cache.log.debug("submitted " + seqs.size()
+ " sequences to " + service.getActionText());
rslt = annotService.submitToService(seqs, getPreset(),
getArguments());
if (rslt == null)
{
return;
}
// TODO: handle job submission error reporting here.
// ///
// otherwise, construct WsJob and any UI handlers
running = new AnnotationWsJob();
running.setJobHandle(rslt);
running.setSeqNames(seqNames);
running.setStartPos(start);
running.setSeqs(seqs);
if (guiProgress != null)
{
guiProgress.registerHandler(progressId,
new IProgressIndicatorHandler()
{
@Override
public boolean cancelActivity(long id)
{
((CancellableI) annotService).cancel(running);
return true;
}
@Override
public boolean canCancel()
{
return cancellable;
}
});
}
// ///
// and poll for updates until job finishes, fails or becomes stale
boolean finished = false;
long rpos = 0;
do
{
Cache.log.debug("Updating status for annotation service.");
annotService.updateStatus(running);
if (running.isFinished())
{
Cache.log.debug("Analysis service job reported finished.");
finished = true;
}
else
{
Cache.log.debug("Status now " + running.getState());
}
if (calcMan.isPending(this) && isInteractiveUpdate())
{
Cache.log.debug("Analysis service job is stale. aborting.");
// job has become stale.
if (!finished) {
finished = true;
// cancel this job and yield to the new job
try
{
if (cancellable
&& ((CancellableI) annotService).cancel(running))
{
System.err.println("Cancelled AACon job: " + rslt);
}
else
{
System.err.println("FAILED TO CANCEL AACon job: " + rslt);
}
} catch (Exception x)
{
}
}
rslt = running.getJobHandle();
return;
}
// pull any stats - some services need to flush log output before
// results are available
Cache.log.debug("Updating progress log for annotation service.");
try
{
annotService.updateJobProgress(running);
} catch (Throwable thr)
{
Cache.log.debug("Ignoring exception during progress update.",
thr);
}
Cache.log.trace("Result of poll: " + running.getStatus());
if (!finished && !running.isFailed())
{
try
{
Cache.log.debug("Analysis service job thread sleeping.");
Thread.sleep(200);
Cache.log.debug("Analysis service job thread woke.");
} catch (InterruptedException x)
{
}
;
}
} while (!finished);
// TODO: need to poll/retry
if (serverErrorsLeft > 0)
{
try
{
Thread.sleep(200);
} catch (InterruptedException x)
{
}
}
// configure job with the associated view's feature renderer, if one
// exists.
// TODO: here one would also grab the 'master feature renderer' in order
// to enable/disable
// features automatically according to user preferences
running.setFeatureRenderer(
((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
Cache.log.debug("retrieving job results.");
List returnedAnnot = annotService
.getAlignmentAnnotation(running, this);
Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
: ("" + returnedAnnot.size())));
running.setAnnotation(returnedAnnot);
if (running.hasResults())
{
jalview.bin.Cache.log.debug("Updating result annotation from Job "
+ rslt + " at " + service.getUri());
updateResultAnnotation(true);
if (running.isTransferSequenceFeatures())
{
jalview.bin.Cache.log.debug(
"Updating feature display settings and transferring features from Job "
+ rslt + " at " + service.getUri());
((jalview.gui.AlignmentPanel) ap)
.updateFeatureRendererFrom(running.getFeatureRenderer());
// TODO: JAL-1150 - create sequence feature settings API for defining
// styles and enabling/disabling feature overlay on alignment panel
if (alignFrame.alignPanel == ap)
{
// only do this if the alignFrame is currently showing this view.
Desktop.getAlignFrameFor(alignViewport)
.setShowSeqFeatures(true);
}
}
ap.adjustAnnotationHeight();
}
Cache.log.debug("Annotation Service Worker thread finished.");
}
// TODO: use service specitic exception handlers
// catch (JobSubmissionException x)
// {
//
// System.err.println(
// "submission error with " + getServiceActionText() + " :");
// x.printStackTrace();
// calcMan.disableWorker(this);
// } catch (ResultNotAvailableException x)
// {
// System.err.println("collection error:\nJob ID: " + rslt);
// x.printStackTrace();
// calcMan.disableWorker(this);
//
// } catch (OutOfMemoryError error)
// {
// calcMan.disableWorker(this);
//
// ap.raiseOOMWarning(getServiceActionText(), error);
// }
catch (Throwable x)
{
calcMan.disableWorker(this);
System.err
.println("Blacklisting worker due to unexpected exception:");
x.printStackTrace();
} finally
{
calcMan.workerComplete(this);
if (ap != null)
{
calcMan.workerComplete(this);
if (guiProgress != null && progressId != -1)
{
guiProgress.setProgressBar("", progressId);
}
// TODO: may not need to paintAlignment again !
ap.paintAlignment(false, false);
}
if (msg.length() > 0)
{
// TODO: stash message somewhere in annotation or alignment view.
// code below shows result in a text box popup
/*
* jalview.gui.CutAndPasteTransfer cap = new
* jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString());
* jalview.gui.Desktop.addInternalFrame(cap,
* "Job Status for "+getServiceActionText(), 600, 400);
*/
}
}
}
/**
* validate input for dynamic/non-dynamic update context TODO: move to
* analysis interface ?
* @param seqs
*
* @return true if input is valid
*/
boolean checkValidInputSeqs(List seqs)
{
int nvalid = 0;
for (SequenceI sq : seqs)
{
if (sq.getStart() <= sq.getEnd()
&& (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
{
if (submitGaps
|| sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
{
nvalid++;
}
}
}
return nvalid >= min_valid_seqs;
}
public void cancelCurrentJob()
{
try
{
String id = running.getJobId();
if (((CancellableI) annotService).cancel(running))
{
System.err.println("Cancelled job " + id);
}
else
{
System.err.println("Job " + id + " couldn't be cancelled.");
}
} catch (Exception q)
{
q.printStackTrace();
}
}
/**
* Interactive updating. Analysis calculations that work on the currently
* displayed alignment data should cancel existing jobs when the input data
* has changed.
*
* @return true if a running job should be cancelled because new input data is
* available for analysis
*/
boolean isInteractiveUpdate()
{
return service.isInteractiveUpdate();
}
/**
* decide what sequences will be analysed TODO: refactor to generate
* List for submission to service interface
*
* @param alignment
* @param inputSeqs
* @return
*/
public List getInputSequences(AlignmentI alignment,
AnnotatedCollectionI inputSeqs)
{
if (alignment == null || alignment.getWidth() <= 0
|| alignment.getSequences() == null || alignment.isNucleotide()
? !nucleotidesAllowed
: !proteinAllowed)
{
return null;
}
if (inputSeqs == null || inputSeqs.getWidth() <= 0
|| inputSeqs.getSequences() == null
|| inputSeqs.getSequences().size() < 1)
{
inputSeqs = alignment;
}
List seqs = new ArrayList<>();
int minlen = 10;
int ln = -1;
if (bySequence)
{
seqNames = new HashMap<>();
}
gapMap = new boolean[0];
start = inputSeqs.getStartRes();
end = inputSeqs.getEndRes();
// TODO: URGENT! unify with JPred / MSA code to handle hidden regions
// correctly
// TODO: push attributes into WsJob instance (so they can be safely
// persisted/restored
for (SequenceI sq : (inputSeqs.getSequences()))
{
if (bySequence
? sq.findPosition(end + 1)
- sq.findPosition(start + 1) > minlen - 1
: sq.getEnd() - sq.getStart() > minlen - 1)
{
String newname = SeqsetUtils.unique_name(seqs.size() + 1);
// make new input sequence with or without gaps
if (seqNames != null)
{
seqNames.put(newname, sq);
}
SequenceI seq;
if (submitGaps)
{
seqs.add(seq = new jalview.datamodel.Sequence(newname,
sq.getSequenceAsString()));
if (gapMap == null || gapMap.length < seq.getLength())
{
boolean[] tg = gapMap;
gapMap = new boolean[seq.getLength()];
System.arraycopy(tg, 0, gapMap, 0, tg.length);
for (int p = tg.length; p < gapMap.length; p++)
{
gapMap[p] = false; // init as a gap
}
}
for (int apos : sq.gapMap())
{
char sqc = sq.getCharAt(apos);
if (!filterNonStandardResidues
|| (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
: ResidueProperties.nucleotideIndex[sqc] < 5))
{
gapMap[apos] = true; // aligned and real amino acid residue
}
;
}
}
else
{
// TODO: add ability to exclude hidden regions
seqs.add(seq = new jalview.datamodel.Sequence(newname,
AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
sq.getSequenceAsString(start, end + 1))));
// for annotation need to also record map to sequence start/end
// position in range
// then transfer back to original sequence on return.
}
if (seq.getLength() > ln)
{
ln = seq.getLength();
}
}
}
if (alignedSeqs && submitGaps)
{
realw = 0;
for (int i = 0; i < gapMap.length; i++)
{
if (gapMap[i])
{
realw++;
}
}
// try real hard to return something submittable
// TODO: some of AAcon measures need a minimum of two or three amino
// acids at each position, and AAcon doesn't gracefully degrade.
for (int p = 0; p < seqs.size(); p++)
{
SequenceI sq = seqs.get(p);
// strip gapped columns
char[] padded = new char[realw],
orig = sq.getSequence();
for (int i = 0, pp = 0; i < realw; pp++)
{
if (gapMap[pp])
{
if (orig.length > pp)
{
padded[i++] = orig[pp];
}
else
{
padded[i++] = '-';
}
}
}
seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
new String(padded)));
}
}
return seqs;
}
@Override
public void updateAnnotation()
{
updateResultAnnotation(false);
}
public void updateResultAnnotation(boolean immediate)
{
if ((immediate || !calcMan.isWorking(this)) && running != null
&& running.hasResults())
{
List ourAnnot = running.getAnnotation();
updateOurAnnots(ourAnnot);
}
}
/**
* notify manager that we have started, and wait for a free calculation slot
*
* @return true if slot is obtained and work still valid, false if another
* thread has done our work for us.
*/
protected boolean checkDone()
{
calcMan.notifyStart(this);
ap.paintAlignment(false, false);
while (!calcMan.notifyWorking(this))
{
if (calcMan.isWorking(this))
{
return true;
}
try
{
if (ap != null)
{
ap.paintAlignment(false, false);
}
Thread.sleep(200);
} catch (Exception ex)
{
ex.printStackTrace();
}
}
if (alignViewport.isClosed())
{
abortAndDestroy();
return true;
}
return false;
}
protected void updateOurAnnots(List ourAnnot)
{
List our = ourAnnots;
ourAnnots = ourAnnot;
AlignmentI alignment = alignViewport.getAlignment();
if (our != null)
{
if (our.size() > 0)
{
for (AlignmentAnnotation an : our)
{
if (!ourAnnots.contains(an))
{
// remove the old annotation
alignment.deleteAnnotation(an);
}
}
}
our.clear();
// validate rows and update Alignmment state
for (AlignmentAnnotation an : ourAnnots)
{
alignViewport.getAlignment().validateAnnotation(an);
}
// TODO: may need a menu refresh after this
// af.setMenusForViewport();
ap.adjustAnnotationHeight();
}
}
public SequenceAnnotationServiceI getService()
{
return annotService;
}
}