package org.juxtasoftware.resource; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.juxtasoftware.dao.AlignmentDao; import org.juxtasoftware.dao.ComparisonSetDao; 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.UserAnnotation; import org.juxtasoftware.model.Witness; import org.juxtasoftware.util.FragmentFormatter; import org.juxtasoftware.util.QNameFilters; import org.juxtasoftware.util.RangedTextReader; import org.restlet.data.Status; import org.restlet.representation.Representation; import org.restlet.resource.Get; 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.JsonObject; import eu.interedition.text.Range; @Service @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class FragmentResource extends BaseResource { @Autowired private ComparisonSetDao setDao; @Autowired private AlignmentDao alignmentDao; @Autowired private WitnessDao witnessDao; @Autowired private QNameFilters filters; @Autowired private UserAnnotationDao userNotesDao; private Long baseWitnessId; private ComparisonSet set; private Range range; private Set<Long> witnessIdList; private static final int MAX_RANGE = 5000; private static final int FRAG_SIZE = 25; @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; } if (getQuery().getValuesMap().containsKey("base") == false ) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Missing base parameter"); } else { String baseId = getQuery().getValues("base"); try { this.baseWitnessId = Long.parseLong(baseId); } catch (NumberFormatException e) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid witness identifier specified"); return; } } if (getQuery().getValuesMap().containsKey("range") == false) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Missing range parameter"); } else { String rangeInfo = getQuery().getValues("range"); String ranges[] = rangeInfo.split(","); if ( ranges.length == 2) { this.range = new Range( Integer.parseInt(ranges[0]), Integer.parseInt(ranges[1]) ); if (this.range.length() > MAX_RANGE) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Range exceeds maximum length of "+MAX_RANGE); } } else { setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid Range specified"); } } // grab the witness id filter. These witness IDs will be filtered out this.witnessIdList = new HashSet<Long>(); if (getQuery().getValuesMap().containsKey("filter") ) { String[] docStrIds = getQuery().getValues("filter").split(","); for ( int i=0; i<docStrIds.length; i++ ) { try { Long witId = Long.parseLong(docStrIds[i]); if ( witId.equals(this.baseWitnessId) ) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Cannot filter out base witness"); return; } else { this.witnessIdList.add(witId); } } catch (Exception e ) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid document id specified"); return; } } } } @Get("json") public Representation toJson() { // get all of the diff alignments in the specified range AlignmentConstraint constraint = new AlignmentConstraint( this.set, this.baseWitnessId ); constraint.setFilter( this.filters.getDifferencesFilter() ); constraint.setRange( this.range ); for (Long witId : this.witnessIdList ) { constraint.addWitnessIdFilter(witId); } List<Alignment> aligns = this.alignmentDao.list( constraint ); // consolidate ranges Map<Long, WitnessFragment > witnessDiffMap = new HashMap<Long, WitnessFragment>(); Map<Long, Witness> witnessMap = new HashMap<Long, Witness>(); for (Alignment align : aligns ) { // if the diff is an addition, and it is on the first char on the range it is // on the line between 2 adjacent changes. count it in the prior change and skip it here AlignedAnnotation baseAnno = align.getWitnessAnnotation(this.baseWitnessId); if ( baseAnno.getRange().length() == 0 && baseAnno.getRange().getStart() == this.range.getStart() && this.range.length() > 0) { continue; } // find the WITNESS portion of this alignment AlignedAnnotation witnessAnno = null; for ( AlignedAnnotation a : align.getAnnotations()) { if ( a.getWitnessId().equals(this.baseWitnessId) == false) { witnessAnno = a; break; } } WitnessFragment info = witnessDiffMap.get(witnessAnno.getWitnessId()); if ( info == null ) { // lookup witness info and cache for use later Witness w = this.witnessDao.find( witnessAnno.getWitnessId() ); witnessMap.put(witnessAnno.getWitnessId(), w); // initialize a new fragment and add it to the fragments map // these fragments have no initial content. will be populated below info = new WitnessFragment( w, witnessAnno.getRange(), align.getEditDistance()); info.witnessId = witnessAnno.getWitnessId(); witnessDiffMap.put(witnessAnno.getWitnessId(), info); } else { info.range = new Range( Math.min(info.getStart(), witnessAnno.getRange().getStart()), Math.max(info.getEnd(), witnessAnno.getRange().getEnd())); } } // Get any user annotations on this range & base id combination UserAnnotation userAnno = this.userNotesDao.find(this.set, this.baseWitnessId, this.range); String groupAnnotation = ""; if (userAnno != null && userAnno.hasGroupAnnotation()) { groupAnnotation = userAnno.getGroupNoteContent(); } // lookup a fragment for each witness for ( Entry<Long, WitnessFragment> entry : witnessDiffMap.entrySet()) { Long witnessId = entry.getKey(); WitnessFragment info = entry.getValue(); Witness w = witnessMap.get( witnessId ); Range fragRange = new Range( Math.max(0, info.range.getStart() - FRAG_SIZE), Math.min(info.range.getEnd()+FRAG_SIZE, w.getText().getLength())); try { final RangedTextReader reader = new RangedTextReader(); reader.read( this.witnessDao.getContentStream(w), fragRange ); info.fragment = FragmentFormatter.format(reader.toString(), info.range, fragRange, w.getText().getLength()); } catch (Exception e) { LOG.error("Error retrieving diff fragment for witness "+witnessId, e); } // see if this particular witness diff has been annotated if ( userAnno != null ) { for ( UserAnnotation.Data noteData : userAnno.getNotes() ) { if ( noteData.getWitnessId().equals(witnessId)) { info.note = noteData.getText(); } } } } JsonObject out = new JsonObject(); out.addProperty("groupAnnotation", groupAnnotation); JsonArray frags = new JsonArray(); out.add("fragments", frags); for ( WitnessFragment f : witnessDiffMap.values() ) { JsonObject obj = new JsonObject(); obj.addProperty("witnessId", f.witnessId); obj.addProperty("witnessName", f.witnessName); obj.addProperty("typeSymbol", f.typeSymbol); obj.addProperty("fragment", f.fragment); obj.addProperty("note", f.note); JsonObject rng = new JsonObject(); rng.addProperty("start", f.getStart()); rng.addProperty("end", f.getEnd()); obj.add("range", rng); frags.add(obj); } String goo = out.toString(); return toJsonRepresentation( goo ); } private static class WitnessFragment { private Range range; private Long witnessId; private String witnessName; private final String typeSymbol; private String fragment; private String note = ""; public WitnessFragment( final Witness witness, final Range r, final int editDist) { this.witnessName = witness.getName(); this.range = r; if ( editDist > -1 ) { this.typeSymbol = "▲ "; } else if ( r.length() == 0 ) { // del this.typeSymbol = "✖ "; } else { // add this.typeSymbol = "✚ "; } } public long getStart() { return this.range.getStart(); } public long getEnd() { return this.range.getEnd(); } } }