package org.juxtasoftware.resource; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.lang.StringEscapeUtils; import org.juxtasoftware.dao.ComparisonSetDao; import org.juxtasoftware.dao.SourceDao; import org.juxtasoftware.dao.WitnessDao; import org.juxtasoftware.model.ComparisonSet; import org.juxtasoftware.model.Source; import org.juxtasoftware.model.Usage; import org.juxtasoftware.model.Witness; import org.juxtasoftware.service.SourceRemover; import org.juxtasoftware.service.SourceTransformer; import org.juxtasoftware.service.importer.ps.ParallelSegmentationImportImpl; import org.juxtasoftware.util.BackgroundTask; import org.juxtasoftware.util.BackgroundTaskCanceledException; import org.juxtasoftware.util.BackgroundTaskStatus; import org.juxtasoftware.util.RangedTextReader; import org.juxtasoftware.util.TaskManager; import org.restlet.data.MediaType; import org.restlet.data.Status; import org.restlet.ext.fileupload.RestletFileUpload; import org.restlet.representation.Representation; import org.restlet.resource.Delete; import org.restlet.resource.Get; import org.restlet.resource.Put; import org.restlet.resource.ResourceException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; import com.google.gson.Gson; import com.google.gson.JsonObject; import eu.interedition.text.Range; @Service @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class SourceResource extends BaseResource { @Autowired private SourceDao sourceDao; @Autowired private ComparisonSetDao setDao; @Autowired private WitnessDao witnessDao; @Autowired private SourceTransformer transformer; @Autowired private TaskManager taskManager; @Autowired private ApplicationContext context; @Autowired private SourceRemover remover; private Range range = null; private Source source; /** * Extract the doc ID from the request attributes. This is the doc that * will be acted upon by the get/delete verbs. */ @Override protected void doInit() throws ResourceException { super.doInit(); Long id = getIdFromAttributes("id"); if ( id == null ) { return; } this.source = this.sourceDao.find(this.workspace.getId(), id); if ( this.source == null ) { setStatus(Status.CLIENT_ERROR_NOT_FOUND, "source "+id+" does not exist"); } // was a range set 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 { getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid Range specified"); } } validateModel( this.source ); } /** * Get the source refreneced by <code>sourceID</code> and return its * XML representation as the response * @return */ @Get("html") public Representation toHtml() throws IOException { final RangedTextReader reader = new RangedTextReader(); reader.read( this.sourceDao.getContentReader(this.source), this.range ); Map<String,Object> map = new HashMap<String,Object>(); map.put("name", this.source.getName()); map.put("sourceId", this.source.getId()); map.put("page", "source"); map.put("title", "Juxta Source: "+this.source.getName()); map.put("text", toTextRepresentation( StringEscapeUtils.escapeHtml(reader.toString()))); return toHtmlRepresentation("source.ftl", map); } /** * Get the source refreneced by <code>sourceID</code> and return its * XML representation as the response * @return */ @Get("txt") public Representation toTxt() throws IOException { final RangedTextReader reader = new RangedTextReader(); reader.read( this.sourceDao.getContentReader(this.source), this.range ); return toTextRepresentation(reader.toString()); } /** * Get the source refreneced by <code>sourceID</code> and return a * json object containing source meta data and complete content * @return */ @Get("json") public Representation toJson() throws IOException { final RangedTextReader reader = new RangedTextReader(); reader.read( this.sourceDao.getContentReader(this.source), this.range ); JsonObject obj = new JsonObject(); obj.addProperty("id", this.source.getId()); obj.addProperty("name", this.source.getName()); obj.addProperty("type", this.source.getType().toString()); obj.addProperty("content", reader.toString()); Gson gson = new Gson(); String out = gson.toJson(obj); return toJsonRepresentation(out); } /** * Update the content of source <code>sourceID</code> with the data * contined in the post * @param entity * @return * @throws ResourceException */ @Put public Representation update( Representation entity ) throws ResourceException { if ( entity == null ) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST); return toTextRepresentation("Missing source payload"); } // extract the input stream from the multipart request if (MediaType.MULTIPART_FORM_DATA.equals(entity.getMediaType(),true)) { InputStream srcInputStream = null; String sourceName= null; boolean isParallelSegmented = false; try { // pull the list of items in this multipart request DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1000240); RestletFileUpload upload = new RestletFileUpload(factory); List<FileItem> items = upload.parseRequest(getRequest()); for ( FileItem item : items ) { if ( item.getFieldName().equals("sourceFile")) { srcInputStream = item.getInputStream(); } else if ( item.getFieldName().equals("sourceName")) { sourceName = item.getString(); } else if ( item.getFieldName().equals("parallelSegmented")) { isParallelSegmented = true; } } // Fail requests with none of the required payload if ( srcInputStream == null && sourceName == null ) { setStatus(Status.CLIENT_ERROR_BAD_REQUEST); return toTextRepresentation("Request is missing all content"); } // Rename source or rename/update content if ( sourceName != null && sourceName.length() > 0 && srcInputStream == null ) { return renameSource( sourceName ); } else { // if no new name was specifed, just pass along the // old name as if it were new. if ( sourceName == null || sourceName.length() == 0) { sourceName = this.source.getName(); } if ( isParallelSegmented ) { return updateParallelSegmentedSource( srcInputStream, sourceName ); } else { return updateSource( srcInputStream, sourceName ); } } } catch (Exception e) { LOG.error("Unable to update source", e); setStatus(Status.CLIENT_ERROR_BAD_REQUEST ); return toTextRepresentation("File upload failed"); } } setStatus(Status.CLIENT_ERROR_BAD_REQUEST); return toTextRepresentation("Unsupported content type in put"); } /** * Update a source that is encoded in TEI Parallel Segmentation. Requires * a re-import of the newly editied source. * * @param srcInputStream * @param sourceName * @return * @throws Exception */ private Representation updateParallelSegmentedSource(InputStream srcInputStream, String newName) throws Exception { TeiPsSourceUpdater updater = new TeiPsSourceUpdater(this.source, srcInputStream, newName); this.taskManager.submit( new UpdateTask(updater)); return toJsonRepresentation( updater.getName() ); } /** * Create a task to update source content and name. This will also find all related witnesses and comparison sets. * Witnesses will be re-parsed and comparison sets will be invalidated (cache cleared, * alignments reset and collated flag set to false) * * @param srcInputStream * @param newName * @return */ private Representation updateSource( InputStream srcInputStream, final String newName ) { List<Usage> usage = this.sourceDao.getUsage(this.source); for (Usage u : usage) { if ( u.getType().equals(Usage.Type.COMPARISON_SET)) { ComparisonSet s = this.setDao.find(u.getId()); if ( s.getStatus().equals(ComparisonSet.Status.COLLATING)) { setStatus(Status.CLIENT_ERROR_CONFLICT); return toTextRepresentation("Cannot update source; related set '"+s.getName()+"' is collating."); } } } SourceUpdater updater = new SourceUpdater(this.source, srcInputStream, newName); this.taskManager.submit( new UpdateTask(updater)); return toJsonRepresentation( updater.getName() ); } /** * Change the name of the source * @param newName * @return */ private Representation renameSource( final String newName ) { this.sourceDao.update(this.source, newName); return toJsonRepresentation("{\"result\": \"success\"}"); } /** * Delete the raw document resource with the ID specified in the request */ @Delete public Representation remove() { try { LOG.info("Delete source "+source.getId()); List<Usage> usage = this.remover.removeSource(this.workspace, this.source); Gson gson = new Gson(); return toJsonRepresentation( gson.toJson(usage)); } catch (ResourceException e) { Status statusCode = e.getStatus(); setStatus( statusCode); return toTextRepresentation( e.getStatus().getDescription() ); } } /** * Interface for an update task */ private interface UpdateExecutor { public void doUpdate( BackgroundTaskStatus status ) throws Exception; public String getName(); } /** * Class to perform all of the steps necessary to update a souce */ private class SourceUpdater implements UpdateExecutor { private InputStream srcInputStream; private String newName; private Source origSource; public SourceUpdater( Source origSource, InputStream srcInputStream, final String newName ) { this.srcInputStream = srcInputStream; this.newName = newName; this.origSource = origSource; } @Override public void doUpdate(BackgroundTaskStatus status) throws Exception { SourceResource.this.sourceDao.update(this.origSource, this.newName, new InputStreamReader(this.srcInputStream)); List<Usage> usage = sourceDao.getUsage(this.origSource); // FIRST pass: clear cached data, alignmsnts and flag set as uncollated for ( Usage use : usage ) { if ( use.getType().equals(Usage.Type.COMPARISON_SET ) ) { ComparisonSet set = SourceResource.this.setDao.find(use.getId()); SourceResource.this.setDao.clearCollationData(set); } } // SECOND PASS: re-transform for ( Usage use : usage ) { if ( use.getType().equals(Usage.Type.WITNESS) ) { Witness oldWit = SourceResource.this.witnessDao.find( use.getId() ); SourceResource.this.transformer.redoTransform(this.origSource, oldWit); } } } @Override public String getName() { final int prime = 31; int result = 1; result = prime * result + this.origSource.getId().hashCode(); return "update-src-"+result; } } /** * Class to perform all of the steps necessary to update and re-import a TEI PS source */ private class TeiPsSourceUpdater implements UpdateExecutor { private InputStream srcInputStream; private String newName; private Source origSource; public TeiPsSourceUpdater( Source origSource, InputStream srcInputStream, String newName ) { this.srcInputStream = srcInputStream; this.newName = newName; this.origSource = origSource; } @Override public void doUpdate(BackgroundTaskStatus status) throws Exception { // First, find the usage of this source and use this to find the set ComparisonSet set = null; for ( Usage u : SourceResource.this.sourceDao.getUsage(this.origSource)) { if ( u.getType().equals(Usage.Type.COMPARISON_SET)) { set = SourceResource.this.setDao.find(u.getId()); break; } } // if the set is still null, this could mean that all of the witnesses that were // created from this source are gone and we can't use them to make a connection to set. // As a fallback, see if a set named the same as the source exists. if ( set == null ) { set = SourceResource.this.setDao.find(SourceResource.this.workspace, this.origSource.getName()); } // End of the line.. if we still have nothing, the all witness AND prior set // have been deleted. Re-create the set. if ( set == null ) { set = new ComparisonSet(); set.setName(this.origSource.getName()); set.setWorkspaceId( SourceResource.this.workspace.getId() ); Long id = SourceResource.this.setDao.create(set); set.setId(id); } // next, update the source with the new text content, then grab a NEW copy // of the source that contains the updated text reference information SourceResource.this.sourceDao.update(this.origSource, newName, new InputStreamReader(srcInputStream)); Source source = SourceResource.this.sourceDao.find(this.origSource.getWorkspaceId(), this.origSource.getId()); // finally, re-import all of the witnesses into the set ParallelSegmentationImportImpl importService = SourceResource.this.context.getBean(ParallelSegmentationImportImpl.class); importService.reimportSource(set, source); } @Override public String getName() { final int prime = 31; int result = 1; result = prime * result + this.origSource.getId().hashCode(); return "update-src-"+result; } } /** * Task to asynchronously execute the source update and push the * changes out to witnesses and comparison sets */ private class UpdateTask implements BackgroundTask { private BackgroundTaskStatus task; private Date startDate; private Date endDate; private UpdateExecutor updateExecutor; private BackgroundTaskStatus.Status status = BackgroundTaskStatus.Status.PENDING; public UpdateTask(UpdateExecutor update) { this.task = new BackgroundTaskStatus( update.getName() ); this.startDate = new Date(); this.updateExecutor = update; } @Override public Type getType() { return BackgroundTask.Type.UPDATE; } @Override public void run() { try { LOG.info("Begin update source task "+this.updateExecutor.getName()); this.status = BackgroundTaskStatus.Status.PROCESSING; this.task.begin(); this.updateExecutor.doUpdate( this.task); LOG.info("task "+this.updateExecutor.getName()+" COMPLETE"); this.endDate = new Date(); this.status = BackgroundTaskStatus.Status.COMPLETE; } catch ( BackgroundTaskCanceledException e) { LOG.info( this.updateExecutor.getName()+" update source task was canceled"); this.endDate = new Date(); this.status = BackgroundTaskStatus.Status.CANCELLED; } catch (Exception e) { LOG.error(this.updateExecutor.getName()+" update source task failed", e); this.task.fail(e.getMessage()); this.endDate = new Date(); this.status = BackgroundTaskStatus.Status.FAILED; } } @Override public void cancel() { this.task.cancel(); } @Override public BackgroundTaskStatus.Status getStatus() { return this.status; } @Override public String getName() { return this.updateExecutor.getName(); } @Override public Date getEndTime() { return this.endDate; } @Override public Date getStartTime() { return this.startDate; } @Override public String getMessage() { return this.task.getNote(); } } }