package org.juxtasoftware.resource;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.juxtasoftware.dao.ComparisonSetDao;
import org.juxtasoftware.dao.JuxtaXsltDao;
import org.juxtasoftware.dao.SourceDao;
import org.juxtasoftware.dao.WitnessDao;
import org.juxtasoftware.model.ComparisonSet;
import org.juxtasoftware.model.JuxtaXslt;
import org.juxtasoftware.model.Source;
import org.juxtasoftware.model.Witness;
import org.juxtasoftware.service.importer.ImportService;
import org.juxtasoftware.service.importer.jxt.JxtImportServiceImpl;
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.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.Post;
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.JsonObject;
import com.google.gson.JsonParser;
/**
* Resource used to import JXT files from desktop to juxta-ws. It also
* supports import of TEI parallel segmentation files.
*
* @author lfoster
*
*/
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class Importer extends BaseResource {
private boolean overwrite = false;
@Autowired private ApplicationContext context;
@Autowired private TaskManager taskManager;
@Autowired private ComparisonSetDao setDao;
@Autowired private WitnessDao witnessDao;
@Autowired private SourceDao sourceDao;
@Autowired private JuxtaXsltDao xsltDao;
@Override
protected void doInit() throws ResourceException {
// was the overwrite flag specified?
if ( getQuery().getValuesMap().containsKey("overwrite")) {
this.overwrite = true;
}
super.doInit();
}
/**
* Accepts a JXT file upload and imports it into the juxta WS data model
*
* @param entity
* @return
*/
@Post
public Representation acceptPost(Representation entity ) {
if ( entity == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return null;
}
if (MediaType.MULTIPART_FORM_DATA.equals(entity.getMediaType(),true)) {
return handleMutipartPost( entity );
} else if ( MediaType.APPLICATION_JSON.equals(entity.getMediaType(),true)) {
return handleJsonPost( entity );
}
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("Invalid post data format");
}
private Representation handleMutipartPost(Representation entity) {
// data for creation of new comparison set
String setName = null;
InputStream is = null;
// Parse the multipart request....
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("setName")) {
setName = item.getString();
} else if ( item.getFieldName().equals("jxtFile")) {
is = item.getInputStream();
}
}
// validate that everything needed is present
if ( setName == null || is == null ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return toTextRepresentation("Missing required JXT parameters");
}
} catch (Exception e) {
LOG.error("Unable to parse multipart data", e);
setStatus(Status.CLIENT_ERROR_BAD_REQUEST );
return toTextRepresentation("File upload failed");
}
try {
// create set to contain the data
ComparisonSet set = createImportSet(setName);
// dump data into new set
ImportService<InputStream> importService = this.context.getBean(JxtImportServiceImpl.class);
BackgroundTask task = new ImportTask<InputStream>(importService, set, is);
this.taskManager.submit(task);
// return set id & task id so its import status can be tracked
JsonObject respJson = new JsonObject();
respJson.addProperty("setId", set.getId().toString());
respJson.addProperty("taskId", task.getName() );
return toJsonRepresentation( respJson.toString() );
} catch (IOException e) {
if ( getStatus() == Status.SUCCESS_OK ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
}
return toTextRepresentation( "Unable to import data: "+e.toString() );
}
}
private Representation handleJsonPost(Representation entity) {
ComparisonSet set = null;
try {
JsonParser parser = new JsonParser();
JsonObject jsonObj = parser.parse( entity.getText()).getAsJsonObject();
String setName = jsonObj.get("setName").getAsString();
Long srcId = jsonObj.get("teiSourceId").getAsLong();
// create source and set
Source src = this.sourceDao.find(this.workspace.getId(), srcId);
set = createImportSet(setName);
// dump data into new set
ImportService<Source> importService = this.context.getBean(ParallelSegmentationImportImpl.class);
BackgroundTask task = new ImportTask<Source>(importService, set, src);
this.taskManager.submit(task);
// return set id & task id so its import status can be tracked
JsonObject respJson = new JsonObject();
respJson.addProperty("setId", set.getId().toString());
respJson.addProperty("taskId", task.getName() );
return toJsonRepresentation( respJson.toString() );
} catch (Exception e) {
if ( set != null ) {
cleanupCanceledImport(set);
}
if ( getStatus() == Status.SUCCESS_OK ) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
}
return toTextRepresentation( "Unable to import data: "+e.toString() );
}
}
private ComparisonSet createImportSet( final String setName) throws IOException {
ComparisonSet set = this.setDao.find(this.workspace, setName);
if ( set != null ) {
if ( this.overwrite == false ) {
setStatus(Status.CLIENT_ERROR_CONFLICT, "Conflict with existing comparison set name");
throw new IOException("Conflict with existing comparison set name");
}
return set;
} else {
set = new ComparisonSet();
set.setName(setName);
set.setWorkspaceId( this.workspace.getId() );
Long id = this.setDao.create(set);
set.setId(id);
return set;
}
}
private static String generateTaskName(Long setId ) {
final int prime = 31;
int result = 1;
result = prime * result + setId.hashCode();
return "import-"+result;
}
private void cleanupCanceledImport( ComparisonSet set ) {
// first, grab the set info so we can have a list of all witnesses
// and their associated metadata.
List<Witness> witnesses = this.setDao.getWitnesses(set);
// kill the set first
this.setDao.delete(set);
// kill the witnesses and associated sources
List<Long> xsltIdList = new ArrayList<Long>();
for ( Witness witness :witnesses ) {
Long sourceId = witness.getSourceId();
xsltIdList.add( witness.getXsltId() );
// Next, kill the witness
this.witnessDao.delete(witness);
// Next, kill the orphaned source
Source s = this.sourceDao.find(this.workspace.getId(), sourceId);
this.sourceDao.delete(s);
}
// Finally, iterate over the XSLTs used and kill them too
for (Long xsltId : xsltIdList ) {
JuxtaXslt xslt = this.xsltDao.find(xsltId);
this.xsltDao.delete(xslt);
}
}
/**
* Task to asynchronously execute the import
*/
private class ImportTask<T> implements BackgroundTask {
private BackgroundTaskStatus task;
private T importSouce;
private ComparisonSet set;
private Date startDate;
private Date endDate;
private String taskName;
private ImportService<T> importer;
private BackgroundTaskStatus.Status status = BackgroundTaskStatus.Status.PENDING;
public ImportTask(ImportService<T> importService, ComparisonSet set, T importSouce) {
this.set = set;
this.importSouce = importSouce;
this.taskName = generateTaskName( set.getId() );
this.task = new BackgroundTaskStatus( this.taskName );
this.startDate = new Date();
this.importer = importService;
}
@Override
public Type getType() {
return BackgroundTask.Type.IMPORT;
}
@Override
public void run() {
try {
LOG.info("Begin task "+this.taskName);
this.status = BackgroundTaskStatus.Status.PROCESSING;
this.task.begin();
this.set.setStatus( ComparisonSet.Status.COLLATING );
Importer.this.setDao.update(this.set);
this.importer.doImport(this.set, this.importSouce, this.task);
LOG.info("task "+this.taskName+" COMPLETE");
this.endDate = new Date();
this.status = BackgroundTaskStatus.Status.COMPLETE;
this.set.setStatus( ComparisonSet.Status.COLLATED );
Importer.this.setDao.update(this.set);
} catch ( BackgroundTaskCanceledException e) {
LOG.info( this.taskName+" task was canceled");
cleanupCanceledImport( this.set );
this.endDate = new Date();
this.status = BackgroundTaskStatus.Status.CANCELLED;
this.set.setStatus( ComparisonSet.Status.NOT_COLLATED );
Importer.this.setDao.update(this.set);
} catch (Exception e) {
LOG.error(this.taskName+" task failed", e);
this.task.fail(e.getMessage());
this.endDate = new Date();
this.status = BackgroundTaskStatus.Status.FAILED;
this.set.setStatus( ComparisonSet.Status.ERROR );
Importer.this.setDao.update(this.set);
}
}
@Override
public void cancel() {
this.task.cancel();
}
@Override
public BackgroundTaskStatus.Status getStatus() {
return this.status;
}
@Override
public String getName() {
return this.taskName;
}
@Override
public Date getEndTime() {
return this.endDate;
}
@Override
public Date getStartTime() {
return this.startDate;
}
@Override
public String getMessage() {
return this.task.getNote();
}
}
}