package org.juxtasoftware.resource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.Set;
import java.util.TreeMap;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.juxtasoftware.dao.AlignmentDao;
import org.juxtasoftware.dao.CacheDao;
import org.juxtasoftware.dao.ComparisonSetDao;
import org.juxtasoftware.dao.PageMarkDao;
import org.juxtasoftware.dao.UserAnnotationDao;
import org.juxtasoftware.dao.WitnessDao;
import org.juxtasoftware.model.Alignment;
import org.juxtasoftware.model.Alignment.AlignedAnnotation;
import org.juxtasoftware.model.AlignmentConstraint;
import org.juxtasoftware.model.ComparisonSet;
import org.juxtasoftware.model.PageMark;
import org.juxtasoftware.model.QNameFilter;
import org.juxtasoftware.model.UserAnnotation;
import org.juxtasoftware.model.Witness;
import org.juxtasoftware.util.BackgroundTask;
import org.juxtasoftware.util.BackgroundTaskCanceledException;
import org.juxtasoftware.util.BackgroundTaskStatus;
import org.juxtasoftware.util.ConversionUtils;
import org.juxtasoftware.util.QNameFilters;
import org.juxtasoftware.util.RangedTextReader;
import org.juxtasoftware.util.TaskManager;
import org.juxtasoftware.util.ftl.FileDirective;
import org.juxtasoftware.util.ftl.FileDirectiveListener;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.FileRepresentation;
import org.restlet.representation.ReaderRepresentation;
import org.restlet.representation.Representation;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.resource.ResourceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import eu.interedition.text.Range;
/**
* Resource used to create an Edition based on a set.
*
* @author loufoster
*
*/
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class EditionBuilderResource extends BaseResource implements FileDirectiveListener {
private enum Format { HTML, DOCX };
@Autowired private PageMarkDao pageMarkDao;
@Autowired private ComparisonSetDao setDao;
@Autowired private QNameFilters filters;
@Autowired private AlignmentDao alignmentDao;
@Autowired private WitnessDao witnessDao;
@Autowired private CacheDao cacheDao;
@Autowired private TaskManager taskManager;
@Autowired private Integer visualizationBatchSize;
@Autowired private UserAnnotationDao userNotesDao;
private ComparisonSet set;
private Long baseWitnessId;
private String editionTitle;
private Integer lineFrequency;
private boolean numberBlankLines;
private List<TaWitness> witnesses = new ArrayList<TaWitness>();
private static final Pattern PUNCTUATION = Pattern.compile("[^a-zA-Z0-9\\-]");
@Override
protected void doInit() throws ResourceException {
super.doInit();
Long id = getIdFromAttributes("id");
if ( id == null ) {
return;
}
this.set = this.setDao.find(id);
if ( validateModel(this.set) == false ) {
return;
}
}
@Get
public Representation get() {
if (getQuery().getValuesMap().containsKey("format") == false ) {
return toTextRepresentation("Missing edition token");
}
long token = -1;
try {
token = Long.parseLong(getQuery().getValuesMap().get("token"));
} catch (Exception e) {
return toTextRepresentation("Invalid edition token format");
}
Format format = Format.HTML;
if (getQuery().getValuesMap().containsKey("format") ) {
format = Format.valueOf(getQuery().getValuesMap().get("format").toUpperCase());
if ( format == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid output format specified. Only HTML and RTF are acceptable.");
}
}
if ( this.cacheDao.editionExists(this.set.getId(), token)) {
Reader rdr = this.cacheDao.getEdition(this.set.getId(), token);
if ( rdr != null ) {
if ( format.equals(Format.HTML)) {
return new ReaderRepresentation( rdr, MediaType.TEXT_HTML);
} else {
return convertHtmlToRtf(rdr);
}
}
}
setStatus(Status.CLIENT_ERROR_NOT_FOUND);
return toTextRepresentation("Invalid edition token");
}
private Representation convertHtmlToRtf(Reader reader) {
try {
File out = ConversionUtils.convertHtmlToDocx(reader);
FileRepresentation rep = new FileRepresentation(out, MediaType.APPLICATION_MSOFFICE_DOCX);
rep.setAutoDeleting(true);
return rep;
} catch (Exception e) {
LOG.error("Convert to DOCX failed", e);
setStatus(Status.SERVER_ERROR_INTERNAL);
return toTextRepresentation("Unable to create docx version of Edition. Try HTML");
}
}
/**
* Create an based on settings present in the JSON payload.
* Expected format:
* {
* format: html|rtf,
* lineNumFrequency: 5,
* witnesses: [
* { id: id, include: true|false, base: true|false, siglum: name }, ...
* ]
* }
* @throws IOException
*/
@Post("json")
public Representation create( final String jsonData) throws IOException {
if ( this.set.getStatus().equals(ComparisonSet.Status.COLLATED) == false ) {
setStatus(Status.CLIENT_ERROR_CONFLICT);
return toTextRepresentation("Unable to generate edition - set is not collated");
}
// parse the config
JsonParser p = new JsonParser();
JsonObject jsonObj = p.parse(jsonData).getAsJsonObject();
this.editionTitle = jsonObj.get("title").getAsString();
this.lineFrequency = jsonObj.get("lineFrequency").getAsInt();
this.numberBlankLines = jsonObj.get("numberBlankLines").getAsBoolean();
JsonArray jsonWits = jsonObj.get("witnesses").getAsJsonArray();
for ( Iterator<JsonElement> itr = jsonWits.iterator(); itr.hasNext(); ) {
JsonObject witInfo = itr.next().getAsJsonObject();
boolean included = witInfo.get("include").getAsBoolean();
boolean isBase = witInfo.get("base").getAsBoolean();
Long witId = witInfo.get("id").getAsLong();
String siglum = witInfo.get("siglum").getAsString();
if ( isBase && this.baseWitnessId != null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("Only one witness can be set as base.");
}
if ( isBase ) {
this.baseWitnessId = witId;
}
Witness w = this.witnessDao.find(witId);
if ( w == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("Invalid witness specified.");
}
if ( this.setDao.isWitness(this.set, w) == false ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("Invalid witness specified.");
}
if ( included ) {
this.witnesses.add( new TaWitness(witId, siglum, w.getName(), isBase));
} else {
this.witnesses.add( new TaWitness(witId));
}
}
if ( this.baseWitnessId == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("No base witness has been specified.");
}
if ( this.witnesses.size() < 2 ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("At least 2 witnesses must be included.");
}
// kick off a task to render the apparatus
final String taskId = "edition-"+set.hashCode();
final long token = System.currentTimeMillis();
if ( this.taskManager.exists(taskId) == false ) {
EditionBuilderTask task = new EditionBuilderTask(taskId, token);
this.taskManager.submit(task);
}
return toJsonRepresentation(
"{\"status\": \"RENDERING\", \"taskId\": \""+taskId+"\", \"token\": \""+token+"\"}" );
}
@Override
public void fileReadComplete(File file) {
file.delete();
}
private void render( final long token ) throws IOException {
// setup: create a tmp file to hold output and get a reader for base witness content
File baseTxt = File.createTempFile("base", "txt");
baseTxt.deleteOnExit();
Witness base = this.witnessDao.find(this.baseWitnessId);
Reader reader = this.witnessDao.getContentStream(base);
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(baseTxt), "UTF-8");
// get the page number marks and select the first one (if available
List<PageMark> lineNums = this.pageMarkDao.find(this.baseWitnessId, PageMark.Type.LINE_NUMBER);
Iterator<PageMark> numItr = lineNums.iterator();
PageMark currNum = null;
if (numItr.hasNext()) {
currNum = numItr.next();
}
// stream witness text from db into file incuding line num markup
// and the line counts based on the line frequency setting. Track the
// range associated with each line number so it can be used to record the
// location of each variant report in the textual apparatus
int pos = 0;
int lineStartPos = 0;
int lineNum = 1;
StringBuilder line = new StringBuilder("");
Map<Range, String> lineRanges = new TreeMap<Range, String>();
String lineLabel = "";
boolean done = false;
boolean lineComplete = false;
while ( !done) {
int data = reader.read();
if (data == -1) {
done = true;
lineComplete = true;
} else {
// save off any line number markup found at this psition.
// it will be handled when the full line has been read
if (currNum != null && currNum.getOffset() == pos) {
lineLabel = currNum.getLabel();
currNum = null;
if (numItr.hasNext()) {
currNum = numItr.next();
}
}
// now handle the actual content read...
if (data == '\n') {
lineComplete = true;
} else {
line.append(StringEscapeUtils.escapeXml(Character.toString((char) data)));
}
}
if ( lineComplete ) {
lineComplete = false;
final int lineLen = line.toString().trim().length();
boolean hasNumberMarkup = true;
if ( lineLabel.length() == 0 ) {
hasNumberMarkup = false;
lineLabel = ""+lineNum;
if ( lineLen == 0 && this.numberBlankLines == false ) {
lineLabel = " ";
}
}
if ( lineLabel.length() > 0 ) {
lineRanges.put(new Range(lineStartPos, pos), lineLabel);
}
if (hasNumberMarkup == false && !(lineNum % this.lineFrequency == 0) ) {
// make sure something is here or blank rows collapse
lineLabel = " ";
}
osw.write("<tr><td class=\"num-col\">"+lineLabel+"</td><td>"+line.toString()+"</td></tr>\n");
if ( hasNumberMarkup == false && (lineLen > 0 || (lineLen == 0 && this.numberBlankLines)) ) {
lineNum++;
}
line = new StringBuilder("");
lineStartPos = pos+1;
lineLabel = "";
}
pos++;
}
IOUtils.closeQuietly(osw);
IOUtils.closeQuietly(reader);
// do all the heavy lifting required to create the textual apparatus
// results will be an html table rendered to the file
File appFile = generateApparatus(lineRanges);
// populate template map and generate HTML
Map<String, Object> map = new HashMap<String, Object>();
map.put("title", this.editionTitle);
map.put("witnesses", this.witnesses);
map.put("baseWitnessText", baseTxt.getAbsoluteFile());
map.put("apparatusFile", appFile.getAbsoluteFile());
FileDirective fd = new FileDirective();
fd.setListener(this);
map.put("fileReader", fd);
Representation rep = this.toHtmlRepresentation("edition.ftl", map, false, false);
this.cacheDao.cacheEdition(this.set.getId(), token, rep.getReader());
}
private File generateApparatus(Map<Range, String> lineRanges) throws IOException {
File appFile = File.createTempFile("app", "txt");
appFile.deleteOnExit();
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(appFile), "UTF-8");
// collect/merge all of the differences into a variant list
List<Variant> variants = generateVariantList();
// build the variant table one line at a time and stream it out to a file
String priorBaseTxt = null;
Set<String> priorWitsWithChange = new HashSet<String>();
for (Variant variant : variants) {
// First, grab the base witness fragment and add it to the variant string
String baseTxt = "";
boolean additionToBase = false;
Range baseRange = new Range( variant.getRange() );
if ( baseRange.length() > 0 ) {
baseTxt = getWitnessText(this.baseWitnessId, baseRange);
baseTxt = baseTxt.replaceAll("\\n+", " / ").replaceAll("\\s+", " ").trim();
} else {
// if base len is 0, this means that a witness added text relative to
// the base. grab the two words from the base text to the left and right of
// this addition to bookend the change. These two words will also
// be appeneded on the ends of the witness text to make it clear that
// the witness added content between them. IMPORTANT: this reach ahead/behind
// opens the possibity of NESTED changes (one witness has a difference from base
// in one of these words). To balance this out, always keep track of the prior
// set of base text and witnesses with diffs. When building the list of
// sigla that are the same as base, this prior info will be consulted.
additionToBase = true;
baseTxt = getBaseAdditionContext( baseRange.getStart());
String[] parts = baseTxt.replaceAll("\\n+", " ").split(" ");
if ( parts.length == 2 ) {
baseRange = new Range(baseRange.getStart()-parts[0].length(), baseRange.getStart()+parts[1].length()+1);
} else {
baseRange = new Range(baseRange.getStart(), baseRange.getStart()+parts[0].length());
}
baseTxt = baseTxt.replaceAll("\\n+", " / ").replaceAll("\\s+", " ").trim();
}
// make it safe
baseTxt = StringEscapeUtils.escapeXml( baseTxt );
// Start the variant string. It is the base text followed by ].
StringBuilder sb = new StringBuilder(baseTxt).append("] ");
// walk thru each of the witnesses that have differences from
// the base at this particular range. Extract the variant text.
boolean nestedChange = false;
Map<String,Set<String>> txtSiglumMap = new HashMap<String,Set<String>>();
for (Entry<Long, Range> ent : variant.getWitnessRangeMap().entrySet()) {
Long witId = ent.getKey();
Range witRng = ent.getValue();
String witTxt = "";
if ( witRng.length() > 0 ) {
if ( additionToBase ) {
// grab the added witness text plus extend out to include
// the prior and next words that book end the change. these
// extra 2 words should match the base text, and serve as bookends
// to delimit the addition
witTxt = getWitnessAdditionWithContext(witId, witRng);
if ( priorBaseTxt != null ){
// if the first part of the bookend text was already
// handled by the prior line of the apparatus. flag
// it here so the witnesss wont get double counted.
nestedChange = witTxt.split(" ")[0].equals(priorBaseTxt);
if ( nestedChange == false && baseTxt.contains(priorBaseTxt)) {
nestedChange = true;
}
}
} else {
witTxt = getWitnessText(witId, witRng);
witTxt = witTxt.replaceAll("\\n+", " / ").replaceAll("\\s+", " ").trim();
}
// clean up for XML
witTxt = StringEscapeUtils.escapeXml( witTxt );
} else {
witTxt = "<i>not in </i>";
}
// accumulate the witness text fragments in a map
// that can associate them with multiple witnesses
final String witSiglum = getSiglum(witId);
if ( txtSiglumMap.containsKey(witTxt)) {
txtSiglumMap.get(witTxt).add(witSiglum);
} else {
Set<String> sigla = new HashSet<String>();
sigla.add(witSiglum);
txtSiglumMap.put(witTxt, sigla);
}
}
// add any witnesses that are NOT accounted for in the txtSiglumMap
// these are witnesses that are the same as the base. add their siglum
// to the witness list following the base fragment.
sb.append("<b>");
addWitnessesMatchingBase(sb, txtSiglumMap, nestedChange, priorWitsWithChange);
// end the base portion of the variant report
sb.append(";</b> ");
// Lastly, use the merged data from above to create the witness variants
boolean first = true;
StringBuilder ids = new StringBuilder();
for (Entry<String, Set<String>> ent : txtSiglumMap.entrySet() ) {
if ( first ) {
first = false;
} else {
sb.append("; ");
}
String witTxt = ent.getKey();
ids = new StringBuilder();
for ( String siglum: ent.getValue() ) {
if ( ids.length() > 0 ) {
ids.append(", ");
} else {
ids.append(" <b>");
}
ids.append(siglum);
}
ids.append("</b>");
sb.append(witTxt).append(ids);
}
// find line num for range
String lineRange = findLineNumber(baseRange, lineRanges);
// shove the whole thing into a table row and write it out to disk
final String out = "<tr><td class=\"num-col\">"+lineRange+"</td><td>"+sb.toString()+"</td></tr>\n";
osw.write( out );
// Add any user annotations on this exact range/base combo
UserAnnotation anno = this.userNotesDao.find(this.set, this.baseWitnessId, baseRange);
if (anno != null ) {
for ( UserAnnotation.Data noteData : anno.getNotes() ) {
StringBuilder a = new StringBuilder();
a.append("<tr><td class=\"num-col\"> </td><td><i>");
if ( anno.hasGroupAnnotation() ) {
StringBuilder g = new StringBuilder();
for (Long id : this.userNotesDao.getGroupWitnesses( anno.getGroupId() ) ) {
if ( g.length() > 0 ) {
g.append(", ");
}
g.append(getSiglum(id));
}
a.append("<b>").append( g ).append("</b>: ");
} else {
a.append( "<b>").append( getSiglum(noteData.getWitnessId()) ).append("</b>: ");
}
a.append(noteData.getText()).append("</i></td></tr>\n");
osw.write( a.toString() );
}
}
// save priors to detect special cases
priorBaseTxt = baseTxt;
priorWitsWithChange.clear();
for ( Set<String> sigla : txtSiglumMap.values()) {
priorWitsWithChange.addAll(sigla);
}
}
IOUtils.closeQuietly(osw);
return appFile;
}
private void addWitnessesMatchingBase(StringBuilder variantSb, Map<String, Set<String>> txtSiglumMap,
boolean nestedChange, Set<String> priorWitsWithChange) {
// merge all of the sigla entries into one list
Set<String> witsWithDiffs = new HashSet<String>();
for ( Set<String> s : txtSiglumMap.values()) {
witsWithDiffs.addAll(s);
}
// if a witness is NOT in the diff list, it has the same
// text as the base. Add it to the sigla list associated
// associated with the base text
StringBuilder sameAsBase = new StringBuilder();
for ( TaWitness w : this.witnesses ) {
boolean alreadyHandled = ( nestedChange && priorWitsWithChange.contains(w.siglum));
if ( witsWithDiffs.contains(w.siglum) == false && alreadyHandled == false ) {
if ( sameAsBase.length() > 0) {
sameAsBase.append(", ");
}
sameAsBase.append(w.siglum);
}
}
variantSb.append(sameAsBase);
}
private List<Variant> generateVariantList() {
boolean done = false;
int startIdx = 0;
List<Variant> variants = new ArrayList<Variant>();
while ( !done ) {
List<Alignment> aligns = getAlignments(startIdx, this.visualizationBatchSize);
if ( aligns.size() < this.visualizationBatchSize ) {
done = true;
} else {
startIdx += this.visualizationBatchSize;
}
// create report based on alignments
for (Alignment align : aligns) {
// get base and witness annotations
AlignedAnnotation baseAnno = align.getWitnessAnnotation(this.baseWitnessId);
AlignedAnnotation witAnno = null;
for ( AlignedAnnotation a : align.getAnnotations()) {
if ( a.getWitnessId().equals(this.baseWitnessId) == false ) {
witAnno = a;
break;
}
}
// look up or create a variant for the current base range
Variant variant = null;
for ( Variant v : variants ) {
if ( v.getRange().equals(baseAnno.getRange())) {
variant = v;
break;
}
// once this range start occurs past the current base range end,
// all of the remaining ranges will be further in the doc. stop now
if ( v.getRange().getStart() > baseAnno.getRange().getEnd() ) {
break;
}
}
if ( variant == null ) {
variant = new Variant(baseAnno.getRange(), align.getGroup());
variants.add(variant);
}
// add the witness info
variant.addWitnessDetail(witAnno.getWitnessId(), witAnno.getRange(), align.getGroup());
}
// merge related variants
Variant prior = null;
for (Iterator<Variant> itr = variants.iterator(); itr.hasNext();) {
Variant variant = itr.next();
if (prior != null) {
// See if these are a candidate to merge
if (variant.hasMatchingGroup(prior) && variant.hasMatchingWitnesses(prior)) {
prior.merge(variant);
itr.remove();
continue;
}
}
prior = variant;
}
}
Collections.sort(variants);
Variant p = null;
for ( Iterator<Variant> vitr = variants.iterator(); vitr.hasNext(); ) {
Variant v = vitr.next();
if ( p != null ) {
if ( p.getRange().equals(v.getRange())) {
p.merge(v);
vitr.remove();
}
}
p = v;
}
return variants;
}
private String getWitnessAdditionWithContext(final Long witId, final Range witRange ) {
final int defaultSize = 40;
Witness w = this.witnessDao.find(witId);
long maxLen = w.getText().getLength();
int start = (int)witRange.getStart();
int end = (int)witRange.getEnd();
int contextSize = defaultSize;
// special case: added at start of doc
if ( start < 10 ) {
Range r = new Range(start, Math.min(maxLen,end+contextSize));
String witTxt = getWitnessText(witId, r);
int spacePos = witTxt.replaceAll("\\n+", " ").indexOf(' ', (end-start)+1);
if ( spacePos > -1 ) {
String added = witTxt.substring(0, (end-start));
String out = added+witTxt.substring((end-start), spacePos);
out = out.replaceAll("\\n+", " / ");
return out.trim();
} else {
return witTxt.replaceAll("\\n+", " / ");
}
}
// special case: added at end of doc
if ( end >= (maxLen-10) ) {
Range r = new Range( Math.max(0,start-contextSize), end);
String witTxt = getWitnessText(witId, r);
int startAdd = contextSize;
String added = witTxt.substring( startAdd );
int p = startAdd;
int spcCnt = 0;
String normalized = witTxt.replaceAll("\\n+", " ");
while (p > 0) {
if ( normalized.charAt(p) == ' ') {
spcCnt++;
if (spcCnt == 2 ) {
break;
}
}
p--;
}
String out= witTxt.substring(p,startAdd)+added;
return out.replaceAll("\\n+", " / ").trim();
}
// don't extend less < 0
contextSize = Math.min(defaultSize, start);
// don't extend past end of doc
long endPos = end + contextSize;
if (endPos >= maxLen) {
long delta = endPos - maxLen;
contextSize -= delta;
}
// get the fragment (including the central word in the witness)
String witTxt = getWitnessText(witId, new Range(start - contextSize, end + contextSize));
String normalized = witTxt.replaceAll("\\n+", " ");
int fragStart = contextSize;
int spcCnt = 0;
while ( fragStart > 0 ) {
if ( normalized.charAt(fragStart) == ' ') {
spcCnt++;
if (spcCnt == 2 ) {
break;
}
}
fragStart--;
}
int fragEnd = contextSize+(end-start);
spcCnt = 0;
while ( fragEnd < normalized.length()) {
if ( normalized.charAt(fragEnd) == ' ') {
spcCnt++;
if (spcCnt == 2 ) {
break;
}
}
fragEnd++;
}
return witTxt.substring(fragStart, fragEnd).trim().replaceAll("\\n+", " / ");
}
private String getBaseAdditionContext(final long pos) {
Witness w = this.witnessDao.find(this.baseWitnessId);
long maxLen = w.getText().getLength();
final int defaultSize = 40;
int contextSize = defaultSize;
if (pos < 10 ) {
Range r = new Range(0, Math.min(maxLen,contextSize));
String witTxt = getWitnessText(this.baseWitnessId, r).trim();
int spcPos = witTxt.replaceAll("\\n+", " ").indexOf(" ");
if ( spcPos > -1 ) {
return witTxt.substring(0, spcPos);
} else {
return witTxt;
}
}
if ( pos >= (maxLen-10) ) {
Range r = new Range( Math.max(0, pos-contextSize), pos);
String witTxt = getWitnessText(this.baseWitnessId, r);
int spcPos = witTxt.replaceAll("\\n+", " ").lastIndexOf(' ');
if ( spcPos > -1 ) {
return witTxt.substring(spcPos);
} else {
return witTxt;
}
}
// don't extend less < 0
contextSize = Math.min(defaultSize, (int) pos);
// don't extend past end of doc
long endPos = pos + contextSize;
if (endPos >= maxLen) {
long delta = endPos - maxLen;
contextSize -= delta;
}
String witTxt = getWitnessText(this.baseWitnessId, new Range(pos - contextSize, pos + contextSize));
String before = witTxt.substring(0, contextSize);
String wb = before.substring(before.replaceAll("\\n+"," ").lastIndexOf(' '));
String after = witTxt.substring(contextSize);
String wa = after;
int start = 0;
while (true) {
int spacePos = after.replaceAll("\\n+"," ").indexOf(' ', start);
if (spacePos > -1) {
wa = after.substring(start, spacePos);
if (wa.length() == 1 && PUNCTUATION.matcher(wa).find()) {
if ( wa.equals("&") ) {
wb = wb + " "+ wa+" ";
} else {
wb = wb+wa+" ";
}
start = spacePos + 1;
} else {
break;
}
} else {
break;
}
}
witTxt = wb + wa;
return witTxt;
}
private String findLineNumber(Range tgtRange, Map<Range, String> lineRanges) {
String startLine = "";
for ( Entry<Range, String> entry : lineRanges.entrySet() ) {
Range r = entry.getKey();
if ( startLine.length() == 0 ) {
if ( tgtRange.getStart() >= r.getStart() && tgtRange.getStart() <= r.getEnd() ) {
startLine = entry.getValue();
if ( tgtRange.getEnd() <= r.getEnd() ) {
return startLine;
}
}
} else {
if ( tgtRange.getEnd() <= r.getEnd() ) {
if ( startLine.trim().length() == 0) {
return entry.getValue();
}
return startLine +"-"+entry.getValue();
}
}
}
return startLine;
}
private String getSiglum( final Long witId ) {
for ( TaWitness w : this.witnesses ) {
if ( w.getId().equals(witId)) {
return w.getSiglum();
}
}
return "UNK";
}
private List<Alignment> getAlignments(int startIdx, int batchSize) {
QNameFilter changesFilter = this.filters.getDifferencesFilter();
AlignmentConstraint constraints = new AlignmentConstraint(this.set, this.baseWitnessId);
// when constraints use a base witness, the filter is EXCLUSIVE.
// add any excluded witnesses to the filter now
for ( TaWitness w : this.witnesses ) {
if ( w.included == false ) {
constraints.addWitnessIdFilter(w.id);
}
}
constraints.setFilter(changesFilter);
constraints.setResultsRange(startIdx, batchSize);
return this.alignmentDao.list(constraints);
}
private String getWitnessText( final Long witId, final Range range ) {
try {
Witness w = this.witnessDao.find(witId);
final RangedTextReader reader = new RangedTextReader();
reader.read( this.witnessDao.getContentStream(w), range );
String out = reader.toString();
return out;
} catch (Exception e) {
LOG.error("Unable to get text for witness "+witId +", "+range, e);
return "";
}
}
private static class Variant implements Comparable<Variant> {
private Set<Integer> groups = new HashSet<Integer>();
private Range range;
private Map<Long, Range> witnessRangeMap = new HashMap<Long, Range>();
public Variant( final Range r, final int groupId) {
this.range = new Range(r);
this.groups.add(groupId);
}
public boolean hasMatchingWitnesses(Variant other) {
for ( Long witId : this.witnessRangeMap.keySet() ) {
if ( other.getWitnessRangeMap().containsKey(witId) == false) {
return false;
}
}
return true;
}
public final Range getRange() {
return this.range;
}
public final Map<Long, Range> getWitnessRangeMap() {
return this.witnessRangeMap;
}
public void addWitnessDetail(Long witnessId, Range range, int group) {
Range witRange = this.witnessRangeMap.get(witnessId);
this.groups.add(group);
if (witRange == null) {
this.witnessRangeMap.put(witnessId, range);
} else {
Range expanded = new Range(
Math.min(witRange.getStart(), range.getStart()),
Math.max(witRange.getEnd(), range.getEnd()));
this.witnessRangeMap.put(witnessId, expanded);
}
}
public boolean hasMatchingGroup(Variant other) {
if ( this.groups.size() != other.groups.size()) {
return false;
}
for ( Integer g1 : this.groups ) {
if ( other.groups.contains(g1) == false) {
return false;
}
}
return true;
}
public void merge( Variant mergeFrom ) {
// new range of this change is the min/max of the two ranges
this.range = new Range(
Math.min( this.range.getStart(), mergeFrom.getRange().getStart() ),
Math.max( this.range.getEnd(), mergeFrom.getRange().getEnd() )
);
this.groups.addAll(mergeFrom.groups);
// for each of the witness details in the merge source, grab the
// details and add them to the details on this change. note that
// all witnesses must match up between mergeFrom and this or the
// merge will not happen. this is enforced in the heatmap render code
for ( Entry<Long, Range> mergeEntry : mergeFrom.getWitnessRangeMap().entrySet()) {
Long witId = mergeEntry.getKey();
Range witRange = mergeEntry.getValue();
Range thisRange = this.witnessRangeMap.get(witId);
if ( thisRange == null ) {
this.witnessRangeMap.put(witId, witRange);
} else {
Range expanded = new Range(
Math.min(witRange.getStart(), thisRange.getStart()),
Math.max(witRange.getEnd(), thisRange.getEnd()));
this.witnessRangeMap.put(witId, expanded);
}
}
}
@Override
public int compareTo(Variant that) {
Range r1 = this.range;
Range r2 = that.range;
if (r1.getStart() < r2.getStart()) {
return -1;
} else if (r1.getStart() > r2.getStart()) {
return 1;
} else {
if (r1.getEnd() < r2.getEnd()) {
return -1;
} else if (r1.getEnd() > r2.getEnd()) {
return 1;
}
}
return 0;
}
}
/**
* Task to asynchronously render the visualization
*/
private class EditionBuilderTask implements BackgroundTask {
private final String name;
private BackgroundTaskStatus status;
private final long token;
private Date startDate;
private Date endDate;
public EditionBuilderTask(final String name, long token) {
this.name = name;
this.token = token;
this.status = new BackgroundTaskStatus( this.name );
this.startDate = new Date();
}
@Override
public Type getType() {
return BackgroundTask.Type.EDITION;
}
@Override
public void run() {
try {
LOG.info("Begin task "+this.name);
this.status.begin();
EditionBuilderResource.this.render( this.token );
LOG.info("Task "+this.name+" COMPLETE");
this.endDate = new Date();
this.status.finish();
} catch ( BackgroundTaskCanceledException e) {
LOG.info( this.name+" task was canceled");
this.endDate = new Date();
} catch (Exception e) {
LOG.error(this.name+" task failed", e);
this.status.fail(e.toString());
this.endDate = new Date();
}
}
@Override
public void cancel() {
this.status.cancel();
}
@Override
public BackgroundTaskStatus.Status getStatus() {
return this.status.getStatus();
}
@Override
public String getName() {
return this.name;
}
@Override
public Date getEndTime() {
return this.endDate;
}
@Override
public Date getStartTime() {
return this.startDate;
}
@Override
public String getMessage() {
return this.status.getNote();
}
}
/**
* Information about a witness in the textual apparatus
*/
public static final class TaWitness {
private final Long id;
private final String siglum;
private final String title;
private final boolean isBase;
private final boolean included;
public TaWitness( Long id) {
this.id = id;
this.siglum = "";
this.title = "";
this.isBase = false;
this.included = false;
}
public TaWitness( Long id, String siglum, String title, boolean base) {
this.id = id;
this.siglum = siglum;
this.title = title;
this.isBase = base;
this.included = true;
}
public Long getId() {
return this.id;
}
public String getSiglum() {
return this.siglum;
}
public String getTitle() {
return this.title;
}
public boolean getIsBase() {
return this.isBase;
}
public boolean getIsIncluded() {
return this.included;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + (included ? 1231 : 1237);
result = prime * result + (isBase ? 1231 : 1237);
result = prime * result + ((siglum == null) ? 0 : siglum.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
TaWitness other = (TaWitness) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (included != other.included)
return false;
if (isBase != other.isBase)
return false;
if (siglum == null) {
if (other.siglum != null)
return false;
} else if (!siglum.equals(other.siglum))
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
return true;
}
}
}