package org.juxtasoftware.resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.UserAnnotation.Data;
import org.juxtasoftware.model.Witness;
import org.juxtasoftware.util.QNameFilters;
import org.juxtasoftware.util.RangedTextReader;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.Delete;
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.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import eu.interedition.text.Range;
/**
* Resource used to manage user annotations on comparison set witness pairs
*
* @author loufoster
*
*/
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class UserAnnotationResource extends BaseResource {
@Autowired private ComparisonSetDao comparionSetDao;
@Autowired private WitnessDao witnessDao;
@Autowired private UserAnnotationDao userNotesDao;
@Autowired private QNameFilters filters;
@Autowired private AlignmentDao alignmentDao;
private ComparisonSet set;
private Range range;
private Long baseId;
private Long witnessId;
@Override
protected void doInit() throws ResourceException {
super.doInit();
Long setId = getIdFromAttributes("id");
if (setId == null) {
return;
}
this.set = this.comparionSetDao.find(setId);
if (validateModel(this.set) == false) {
return;
}
// was a base specified?
if ( getQuery().getValuesMap().containsKey("base") ) {
String strVal = getQuery().getValues("base");
try {
this.baseId = Long.parseLong(strVal);
} catch (NumberFormatException e) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid base identifier specified");
}
}
// was a witness specified?
if ( getQuery().getValuesMap().containsKey("witness") ) {
String strVal = getQuery().getValues("witness");
try {
this.witnessId = Long.parseLong(strVal);
} catch (NumberFormatException e) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid base identifier specified");
}
}
// was a range requested?
if (getQuery().getValuesMap().containsKey("range")) {
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]));
} else {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid Range specified");
}
}
}
@Post("json")
public Representation create( final String jsonData ) {
UserAnnotation newAnno = parseRequest(jsonData);
if ( newAnno == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("Malformed json payload");
}
newAnno.setSetId(this.set.getId());
if ( newAnno.getBaseId() == null || newAnno.getNotes().size() == 0 || newAnno.getBaseRange() == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("Missing required data in json payload");
}
// Handle updates and additions to existing user annotation
UserAnnotation prior = this.userNotesDao.find(this.set, newAnno.getBaseId(), newAnno.getBaseRange());
if ( prior != null ) {
// newly added group annos have to skip this bit and be added as new at the
// bottom of this function
boolean newAddition = (prior.hasGroupAnnotation() != newAnno.hasGroupAnnotation() &&
prior.hasWitnessAnnotation() != newAnno.hasWitnessAnnotation() );
if ( !newAddition ) {
boolean handled = false;
Data newNote = (Data) newAnno.getNotes().toArray()[0];
for ( Data n : prior.getNotes() ) {
if ( n.getWitnessId().equals(newNote.getWitnessId())) {
n.setText(newNote.getText());
handled = true;
if (n.isGroup()) {
Long groupId = this.userNotesDao.findGroupId(this.set, newAnno.getBaseId(), newAnno.getBaseRange());
this.userNotesDao.updateGroupNote(groupId, newAnno.getGroupNoteContent());
} else {
this.userNotesDao.updateWitnessNote(n.getId(), newNote.getText());
}
}
}
// if the above loop didn't catch the new data
// add it as a separate note
if ( handled == false ) {
this.userNotesDao.addWitnessNote(prior, newNote.getWitnessId(), newNote.getText());
}
return toTextRepresentation("OK");
}
}
// a wholly new user annotation
Long id = this.userNotesDao.create(newAnno);
if ( newAnno.hasGroupAnnotation() ) {
newAnno.setId(id);
newAnno.setGroupId(id);
this.userNotesDao.updateGroupId(newAnno, id);
createReciprocalAnnotations( newAnno );
}
return toTextRepresentation("OK");
}
private UserAnnotation parseRequest(String jsonData) {
JsonParser p = new JsonParser();
JsonObject obj = p.parse(jsonData).getAsJsonObject();
UserAnnotation anno = new UserAnnotation();
anno.setBaseId( obj.get("baseId").getAsLong() );
JsonObject rngObj = obj.get("baseRange").getAsJsonObject();
anno.setBaseRange( new Range( rngObj.get("start").getAsLong(), rngObj.get("end").getAsLong() ));
Data note = UserAnnotation.createNote(obj.get("witnessId").getAsLong(), obj.get("note").getAsString());
note.setGroup(obj.get("isGroup").getAsBoolean());
anno.addNote( note );
return anno;
}
private void createReciprocalAnnotations( UserAnnotation groupAnno ) {
// get all of the diff alignments in the specified range
AlignmentConstraint constraint = new AlignmentConstraint( this.set, groupAnno.getBaseId() );
constraint.setFilter( this.filters.getDifferencesFilter() );
constraint.setRange( groupAnno.getBaseRange() );
List<Alignment> aligns = this.alignmentDao.list( constraint );
// consolidate ranges
Map<Long, Range > witRangeMap = new HashMap<Long, Range>();
for (Alignment align : aligns ) {
AlignedAnnotation witnessAnno = null;
for ( AlignedAnnotation a : align.getAnnotations()) {
if ( a.getWitnessId().equals(groupAnno.getBaseId()) == false) {
witnessAnno = a;
break;
}
}
Range range = witRangeMap.get(witnessAnno.getWitnessId());
if ( range == null ) {
witRangeMap.put(witnessAnno.getWitnessId(), witnessAnno.getRange());
} else {
witRangeMap.put(witnessAnno.getWitnessId(), new Range(
Math.min(range.getStart(), witnessAnno.getRange().getStart()),
Math.max(range.getEnd(), witnessAnno.getRange().getEnd())) );
}
}
for ( Entry<Long, Range> ent : witRangeMap.entrySet() ) {
UserAnnotation a = new UserAnnotation();
Data note = UserAnnotation.createNote(ent.getKey(), groupAnno.getGroupNoteContent() );
note.setGroup(true);
a.addNote( note );
a.setBaseId(ent.getKey());
a.setSetId(this.set.getId());
a.setBaseRange(ent.getValue());
a.setGroupId( groupAnno.getGroupId() );
this.userNotesDao.create(a);
}
}
@Get("html")
public Representation getHtml() {
List<UserAnnotation> ua = getUserAnnotations();
// replace the name of any group notes with the names of
// all witnesses that ahve been annotated
List<Witness> wits = this.comparionSetDao.getWitnesses(this.set);
for (UserAnnotation a : ua ) {
if ( a.hasGroupAnnotation() ) {
for (Data n : a.getNotes() ) {
if ( n.isGroup() ) {
StringBuilder newTitle = new StringBuilder();
for (Long witId : this.userNotesDao.getGroupWitnesses(a.getGroupId()) ) {
for ( Witness w : wits ) {
if ( w.getId().equals(witId)) {
if ( newTitle.length() != 0) {
newTitle.append("<br/>");
}
newTitle.append(w.getName());
break;
}
}
}
n.setWitnessName(newTitle.toString());
}
}
}
}
Map<String,Object> map = new HashMap<String,Object>();
map.put("annotations", ua);
return toHtmlRepresentation("user_annotations.ftl", map,false);
}
@Get("json")
public Representation getJson() {
List<UserAnnotation> ua = getUserAnnotations();
Gson gson = new Gson();
String out = gson.toJson(ua);
return toJsonRepresentation( out );
}
private List<UserAnnotation> getUserAnnotations() {
List<UserAnnotation> ua = this.userNotesDao.list(this.set, this.baseId);
List<Witness> witnesses = this.comparionSetDao.getWitnesses(this.set);
for ( UserAnnotation a : ua ) {
a.setBaseFragment( getBaseFragment(a.getBaseId(), a.getBaseRange()) );
for (Iterator<UserAnnotation.Data> itr = a.getNotes().iterator(); itr.hasNext(); ) {
UserAnnotation.Data note = itr.next();
if ( this.witnessId != null ) {
if ( note.getWitnessId().equals(this.witnessId) == false) {
itr.remove();
continue;
}
}
note.setWitnessName(findName(witnesses, note.getWitnessId()));
}
}
return ua;
}
private String getBaseFragment(Long baseId, Range baseRange) {
final int contextSize=20;
Witness base = this.witnessDao.find(baseId);
Range tgtRange = new Range(
Math.max(0, baseRange.getStart()-contextSize),
Math.min(base.getText().getLength(), baseRange.getEnd()+contextSize));
try {
// read the full fragment
final RangedTextReader reader = new RangedTextReader();
reader.read( this.witnessDao.getContentStream(base), tgtRange );
String frag = reader.toString().trim();
int pos = frag.indexOf(' ');
if ( pos > -1 ) {
frag = "..."+frag.substring(pos+1);
}
pos = frag.lastIndexOf(' ');
if ( pos > -1 ) {
frag = frag.substring(0,pos)+"...";
}
frag = frag.replaceAll("\\n+", " / ").replaceAll("\\s+", " ").trim();
return frag;
} catch (IOException e) {
// couldn't get fragment. skip it for now
return "";
}
}
private String findName(List<Witness> wl, Long id) {
for ( Witness w : wl ) {
if (w.getId().equals(id) ) {
return w.getJsonName();
}
}
return "";
}
@Delete("json")
public Representation deleteUserAnnotation( ) {
// make sure someting exists to be deleted
UserAnnotation tgtAnno = this.userNotesDao.find( this.set, this.baseId, this.range );
if ( tgtAnno == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("No matching user annotation found");
}
// blanket delete of all?
if (this.witnessId == null) {
this.userNotesDao.delete( this.set, this.baseId, this.range );
return toTextRepresentation(""+this.userNotesDao.count(this.set, baseId));
}
// Group annotation delete
if ( this.witnessId == 0 ) {
// Flag attempt to delete group anno where none exist
if (tgtAnno.hasGroupAnnotation() == false ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("No matching user annotation found");
}
this.userNotesDao.deleteGroupNote(this.set, tgtAnno.getGroupId());
return toTextRepresentation(""+this.userNotesDao.count(this.set, baseId));
}
// single delete
// Just remove the specified note from the base range.
for (Iterator<UserAnnotation.Data> itr = tgtAnno.getNotes().iterator(); itr.hasNext();) {
UserAnnotation.Data note = itr.next();
if (this.witnessId.equals(note.getWitnessId())) {
itr.remove();
this.userNotesDao.deleteWitnessNote(note.getId());
}
}
return toTextRepresentation(""+this.userNotesDao.count(this.set, baseId));
}
public static class FragmentInfo {
private final String frag;
private final Range r;
public FragmentInfo(Range r, String f) {
this.frag = f;
this.r = new Range(r.getStart(), r.getEnd());
}
public Range getRange() {
return this.r;
}
public String getFragment() {
return this.frag;
}
}
}