@Component
public class SpringBatchRunner {

	@Autowired
	@Qualifier(BatchConstants.ASYNC_JOB_LAUNCHER)
	JobLauncher jobLauncher;

	@Autowired
	@Qualifier(BatchConstants.BPEG_CONVERTOR_JOB)
	private Job bpegCreateJob;

	@Autowired
	private JSonService jsonService;

	@Autowired
	ProcessDataService dataService;

	public JobExecution runBpgGeneratorJob(ProcessData data) throws Exception {

		dataService.saveOrUpdate(data);

		String processData = jsonService.processData2json(data);

		JobParameters jobParameters = createBpgGeneratorParameters(data, processData);

		JobExecution firstExecution = jobLauncher.run(bpegCreateJob, jobParameters);

		return firstExecution;
	}

	protected JobParameters createBpgGeneratorParameters(ProcessData data, String processData) {
		Map<String, JobParameter> map = new HashMap<String, JobParameter>();
		map.put(BatchConstants.PROCESS_DATA_UUID, new JobParameter(data.getId()));

		// TODO do something for processData values
		JobParameters jobParameters = new JobParameters(map);
		return jobParameters;
	}

} public class BpgGeneratorJob extends AbstractProcessDataManager {

	final static Logger logger = LoggerFactory.getLogger(BpgGeneratorJob.class);

	@Value("${output.dir}")
	private org.springframework.core.io.Resource outputDir;

	@Autowired
	private InstantService instantService;

	@Autowired
	private BpgSystemCommandStep bpgConvertor;

	@Autowired
	private ImageCheckFilterStep imageCheckFilterStep;

	@Autowired
	private NotifyEncodingSuccessStep encodingSuccessStep;

	@Autowired
	private RemoveDataStep removeDataStep;

	@Autowired
	protected FileResourceService fileResourceService;

	@Async
	public void process(ProcessData data) throws BusinessException {
		try {
			// //////////////////////// CHECK
			// create check action
			logger.info("check data...");

			update(data);

			if (imageCheckFilterStep.accept(data)) {

				logger.info("check data DONE.");
				// //////////////////////// CONVERT
				logger.info("convert data...");

				Future<StepState> state = bpgConvertor.process(data);

				Instant start = instantService.now();
				if (StepState.SUCCESS.equals(state.get())) {
					data.setEncoded(true);
					data.setBpgSize(fileResourceService.getBpgFileSize(data.getId()));
					Instant end= Instant.now();
					data.setEncodingDuration(Duration.between(start, end));
					logger.info("convert DONE.");
					
					// //////////////////////// SCHEDULE REMOVE DATA
					removeDataStep.process(data);

					// //////////////////////// NOTIFY
					logger.info("notify success...");

					encodingSuccessStep.notify(data);
				} else {
					logger.info("notify failure...");

				}
				logger.info("notify DONE.");
			}
			update(data);
		} catch (BusinessException e) {
			logger.info(e.getData().toString());
			e.setProcessData(data);
			throw e;
		} catch (ExecutionException | InterruptedException e) {
			logger.info(e.getMessage(), e);
			throw new TechnicalException(ExceptionData.THREAD_ENCODING_ERROR, data, e);
		}
	}

} @Component
public class NotifyFailureStep extends NotifyStep{

	
} public class BusinessException extends Exception implements ProcessDataHolder, ExceptionDataHolder {

	private static final long serialVersionUID = 1L;

	private ExceptionData data;
	private ProcessData processData;

	public BusinessException(ExceptionData data) {
		super();
		this.data = data;
	}

	public BusinessException(ExceptionData data, Exception e) {
		super(e);
		this.data = data;
	}

	public BusinessException(ExceptionData data, ProcessData processData) {
		super();
		this.data = data;
		this.processData = processData;
	}

	public BusinessException(ExceptionData data, ProcessData processData, Exception e) {
		super(e);
		this.data = data;
		this.processData = processData;
	}

	public ExceptionData getData() {
		return data;
	}

	public void setData(ExceptionData data) {
		this.data = data;
	}

	public ProcessData getProcessData() {
		return processData;
	}

	public void setProcessData(ProcessData processData) {
		this.processData = processData;
	}

	@Override
	public String getMessage() {
		StringBuilder sb = new StringBuilder();
		if (super.getMessage() != null) {
			sb.append(super.getMessage() + " : ");
		}
		sb.append(toString());
		return sb.toString();
	}

	@Override
	public String toString() {
		return "BusinessException [data=" + data + ", processData=" + processData + ", getCause()=" + getCause() + "]";
	}

} public class TechnicalException extends RuntimeException implements ExceptionDataHolder, ProcessDataHolder {

	private static final long serialVersionUID = 1L;

	private ExceptionData data;
	private ProcessData processData;
	private String message;

	public TechnicalException(ExceptionData data, String message) {
		super();
		this.data = data;
		this.message = message;
	}

	public TechnicalException(ExceptionData data) {
		super();
		this.data = data;
	}

	public TechnicalException(ExceptionData data, Exception cause) {
		super(cause);
		this.data = data;
	}

	public TechnicalException(ExceptionData data, ProcessData processData) {
		super();
		this.data = data;
		this.processData = processData;
	}

	public TechnicalException(ExceptionData data, ProcessData processData, Exception cause) {
		super(cause);
		this.data = data;
		this.processData = processData;
	}

	public ExceptionData getData() {
		return data;
	}

	public void setData(ExceptionData data) {
		this.data = data;
	}

	public ProcessData getProcessData() {
		return processData;
	}

	public void setProcessData(ProcessData processData) {
		this.processData = processData;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

} public class AppSession {

	private String sessionId;
	private int maxProcessData;
	private Map<String, ProcessData> pmap = new HashMap<String, ProcessData>();

	public AppSession(String sessionId, int maxProcessData) {
		this.sessionId = sessionId;
		this.maxProcessData = maxProcessData;
	}

	public ProcessData getProcessData(String pid) {
		return pmap.get(pid);
	}

	public boolean putProcessData(String pid, ProcessData data) {
		if (pmap.containsKey(pid) || pmap.values().size() <= maxProcessData) {
			pmap.put(pid, data);
			return true;
		}
		return false;
	}

	public String getSessionId() {
		return sessionId;
	}

	public void setSessionId(String sessionId) {
		this.sessionId = sessionId;
	}

	public int getMaxProcessData() {
		return maxProcessData;
	}

	public void setMaxProcessData(int maxProcessData) {
		this.maxProcessData = maxProcessData;
	}

	public Collection<ProcessData> getProcessDatas() {
		return pmap.values();
	}
	
} public enum ExceptionData implements Serializable {

	RESOURCE_CREATE("T100", "Problem when creating a resource"), 
	RESOURCE_DELETE("T101", "Problem when deleting a resource"), 
	RESOURCE_DOWNLOAD("T102", "Problem when downloading a resource"), 
	RESOURCE_NO_DOWNLOAD("T102", "No resource to download"), 
	RESOURCE_UPLOAD("T103", "Problem when uploading a resource"), 
	SPRING_BATCH_ISE("T200", "Spring batch illegal state exception"), 
	JSON_MAPPING_ERROR("T300", "Problem during json2Objec or object2Json transformation"), 
	THREAD_ENCODING_ERROR("T300", "Problem with thread during encoding"), 
	BPG_COMMAND_PARAMETER_ERROR("400", "Problem with the encoding command parameter"), 

	BPGENC_ERROR("F100", "Problem with the bpegenc encoding process"),
	INPUT_IMAGE_CHECK_ERROR("F200", "Problem with the input message"),
	MAX_FILE_PER_SESSION_EXCEED("F300", "The maximum file upload for this session has been reached. Please update the web page and identify a new captcha."),
	NO_SESSION("F301", "The session does not exist."),
	NO_PROCESS_DATA("F301", "The process data does not exist.");

	private String code;
	private String message;

	private ExceptionData(String code, String message) {
		this.code = code;
		this.message = message;
	}

	public String getCode() {
		return code;
	}

	public String getMessage() {
		return message;
	}

	@Override
	public String toString() {
		return "ExceptionData[code=" + code + ",message=" + message + "]";
	}

} @Entity
public class ProcessData implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	private String id;
	private Instant created;
	private Instant lastOperation;
	private ProcessAction action;
	private String originalFilename;
	private String visibleOutputFilename;
	private String sessionId;
	private Long bpgSize;
	private Duration encodingDuration;
	private boolean encoded;
	private String error;
	private boolean clientAjaxNotified = false;

	// bpg encoder parameters
	private int bpgQuantizer = 28;
	private int bpgChroma = 420;
	private String bpgColorSpace = "ycbcr";
	private int bpgLevel = 8;
	private int bpgBitDepth = 8;

	private List<String> steps = new ArrayList<String>();

	public ProcessData() {
	}

	public String getOriginalFilename() {
		return originalFilename;
	}

	public void setOriginalFilename(String originalFilename) {
		this.originalFilename = originalFilename;
	}

	public ProcessAction getAction() {
		return action;
	}

	public void setAction(ProcessAction action) {
		this.action = action;
	}

	public Instant getCreated() {
		return created;
	}

	public void setCreated(Instant instant) {
		this.created = instant;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getVisibleOutputFilename() {
		return visibleOutputFilename;
	}

	public void setVisibleOutputFilename(String visibleOutputFilename) {
		this.visibleOutputFilename = visibleOutputFilename;
	}

	public Instant getLastOperation() {
		return lastOperation;
	}

	public void setLastOperation(Instant lastOperation) {
		this.lastOperation = lastOperation;
	}

	public List<String> getSteps() {
		return steps;
	}

	public void setSteps(List<String> steps) {
		this.steps = steps;
	}

	public String getSessionId() {
		return sessionId;
	}

	public void setSessionId(String sessionId) {
		this.sessionId = sessionId;
	}

	@Override
	public String toString() {
		return "ProcessData [created=" + created + ", lastOperation=" + lastOperation + ", id=" + id + ", action="
				+ action + ", originalFilename=" + originalFilename + ", visibleOutputFilename=" + visibleOutputFilename
				+ ", sessionId=" + sessionId + ", bpgSize=" + bpgSize + ", encodingDuration=" + encodingDuration
				+ ", encoded=" + encoded + ", error=" + error + ", clientAjaxNotified=" + clientAjaxNotified
				+ ", quantizer=" + bpgQuantizer + ", chroma=" + bpgChroma + ", colorSpace=" + bpgColorSpace + ", level="
				+ bpgLevel + ", bitDepth=" + bpgBitDepth + ", steps=" + steps + "]";
	}

	public boolean isEncoded() {
		return encoded;
	}

	public void setEncoded(boolean encoded) {
		this.encoded = encoded;
	}

	public String getError() {
		return error;
	}

	public void setError(String error) {
		this.error = error;
	}

	public boolean isClientAjaxNotified() {
		return clientAjaxNotified;
	}

	public void setClientAjaxNotified(boolean clientAjaxNotified) {
		this.clientAjaxNotified = clientAjaxNotified;
	}

	public Long getBpgSize() {
		return bpgSize;
	}

	public void setBpgSize(Long bpgSize) {
		this.bpgSize = bpgSize;
	}

	public Duration getEncodingDuration() {
		return encodingDuration;
	}

	public void setEncodingDuration(Duration encodingDuration) {
		this.encodingDuration = encodingDuration;
	}

	public int getBpgQuantizer() {
		return bpgQuantizer;
	}

	public void setBpgQuantizer(int quantizer) {
		this.bpgQuantizer = quantizer;
	}

	public int getBpgChroma() {
		return bpgChroma;
	}

	public void setBpgChroma(int chroma) {
		this.bpgChroma = chroma;
	}

	public String getBpgColorSpace() {
		return bpgColorSpace;
	}

	public void setBpgColorSpace(String colorSpace) {
		this.bpgColorSpace = colorSpace;
	}

	public int getBpgLevel() {
		return bpgLevel;
	}

	public void setBpgLevel(int level) {
		this.bpgLevel = level;
	}

	public int getBpgBitDepth() {
		return bpgBitDepth;
	}

	public void setBpgBitDepth(int bitDepth) {
		this.bpgBitDepth = bitDepth;
	}

} public class UploadedFile implements Serializable {

	private static final long serialVersionUID = 1L;

	private String uuid;
	private String suuid;
	private String name;
	private String bpgname;
	private Integer size;
	private Long bpgSize;
	private String encodingTime;
	private String url;
	private String thumbnail_url;
	private String delete_url;
	private String delete_type;
	private String error;
	private boolean encoded;
	
	public UploadedFile() {
		super();
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getSize() {
		return size;
	}

	public void setSize(Integer size) {
		this.size = size;
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public String getThumbnail_url() {
		return thumbnail_url;
	}

	public void setThumbnail_url(String thumbnail_url) {
		this.thumbnail_url = thumbnail_url;
	}

	public String getDelete_url() {
		return delete_url;
	}

	public void setDelete_url(String delete_url) {
		this.delete_url = delete_url;
	}

	public String getDelete_type() {
		return delete_type;
	}

	public void setDelete_type(String delete_type) {
		this.delete_type = delete_type;
	}

	@Override
	public String toString() {
		return "UploadedFile [uuid=" + uuid + ", suuid=" + suuid + ", name=" + name + ", bpgname=" + bpgname
				+ ", size=" + size + ", bpgSize=" + bpgSize + ", encodingTime=" + encodingTime + ", url=" + url
				+ ", thumbnail_url=" + thumbnail_url + ", delete_url=" + delete_url + ", delete_type=" + delete_type
				+ ", error=" + error + ", encoded=" + encoded + "]";
	}

	public String getError() {
		return error;
	}

	public void setError(String error) {
		this.error = error;
	}

	public String getBpgname() {
		return bpgname;
	}

	public void setBpgname(String bpgname) {
		this.bpgname = bpgname;
	}

	public String getUuid() {
		return uuid;
	}

	public void setUuid(String id) {
		this.uuid = id;
	}

	public String getSuuid() {
		return suuid;
	}

	public void setSuuid(String suuid) {
		this.suuid = suuid;
	}

	public boolean isEncoded() {
		return encoded;
	}

	public void setEncoded(boolean encoded) {
		this.encoded = encoded;
	}

	public Long getBpgSize() {
		return bpgSize;
	}

	public void setBpgSize(Long bpgSize) {
		this.bpgSize = bpgSize;
	}

	public String getEncodingTime() {
		return encodingTime;
	}

	public void setEncodingTime(String encodingTime) {
		this.encodingTime = encodingTime;
	}

} public class UploadedFiles implements Serializable {

	private static final long serialVersionUID = 1L;
	private List<UploadedFile> files = new ArrayList<UploadedFile>();

	public UploadedFiles() {
	}

	public List<UploadedFile> getFiles() {
		return files;
	}

	public void setFiles(List<UploadedFile> files) {
		this.files = files;
	}

} @Component
public class AppSessionService {

	final static Logger logger = LoggerFactory.getLogger(AppSessionService.class);

	@Value("${session.max.upload}")
	private Integer sessionMaxUpload = 10;

	private Map<String, AppSession> map = new HashMap<String, AppSession>();

	public AppSession getSession(String sessionId) {
		AppSession appSession = map.get(sessionId);
		if (appSession == null) {
			appSession = new AppSession(sessionId, sessionMaxUpload);
			map.put(sessionId, appSession);
		}
		return appSession;
	}

	public AppSession putProcessData(String sessionId, ProcessData data) throws BusinessException {
		AppSession appSession = map.get(sessionId);
		if (appSession != null) {
			if (appSession.putProcessData(data.getId(), data)) {
				return appSession;
			}
			throw new BusinessException(ExceptionData.MAX_FILE_PER_SESSION_EXCEED, data);

		}
		throw new BusinessException(ExceptionData.NO_SESSION, data);
	}

	public boolean updateProcessData(String sessionId, ProcessData data) {
		AppSession appSession = map.get(sessionId);
		if (appSession != null) {
			return appSession.putProcessData(data.getId(), data);
		}
		return false;
	}

	public ProcessData getProcessData(String sessionId, String dataId) {
		AppSession appSession = map.get(sessionId);
		if (appSession != null) {
			return appSession.getProcessData(dataId);
		}
		return null;
	}

} @Component
public class FileResourceService {

	final static Logger logger = LoggerFactory.getLogger(FileResourceService.class);

	@Value("${upload.dir}")
	private Resource uploadDir;

	@Value("${output.dir}")
	private Resource outputDir;

	@PostConstruct
	protected void init() {

		if (uploadDir == null || outputDir == null) {
			String msg = "the input and output dir must be set! Check application.properties, property upload.dir and output.dir.";
			logger.error(msg);
			throw new IllegalStateException(msg);
		}
	}

	protected Resource createRelative(Resource resource, String filename) {
		try {
			return resource.createRelative(filename);
		} catch (IOException e) {
			logger.warn("" + ExceptionData.RESOURCE_CREATE);
			throw new TechnicalException(ExceptionData.RESOURCE_CREATE, e);
		}
	}

	public Resource createBpg(String filename) {
		String file = filename;
		if (!file.toUpperCase().endsWith(".BPG")) {
			file = file + ".bpg";
		}
		return createRelative(outputDir, file);
	}

	public long getBpgFileSize(String filename) {
		long l;
		try {
			l = createBpg(filename).getFile().length();
		} catch (IOException e) {
			logger.warn("Problem when retrieving the file size");
			l = -1;
		}
		return l;
	}

	public Resource createUpload(String filename) {
		return createRelative(uploadDir, filename);
	}

	public void deleteUpload(String id) {
		delete(createUpload(id));
	}

	public void deleteOutput(String id) {
		delete(createBpg(id));
	}

	protected void delete(Resource resource) {
		try {
			if (resource.exists()) {
				resource.getFile().delete();
			}
		} catch (IOException e) {
			logger.warn("" + ExceptionData.RESOURCE_DELETE);
			throw new TechnicalException(ExceptionData.RESOURCE_DELETE, e);
		}

	}

} @Component
public class InstantService {
	public Instant now() {
		return Instant.now();
	}	
} @Service
public class ProcessDataService {

	@Autowired
	private InstantService instantService;
	@Autowired
	private AppSessionService appSessionService;
	@Autowired
	HashService hashService;

	private Map<String,ProcessData> database = new HashMap<>();
	
	public ProcessData createProcessData(String originalFilename, String sessionId) throws BusinessException {
		String uuid = hashService.createHashCode();
		ProcessData data = new ProcessData();
		data.setCreated(instantService.now());
		data.setId(uuid);
		appSessionService.putProcessData(sessionId, data);
		data.setOriginalFilename(originalFilename);
		data.setSessionId(sessionId);
		String visible = FilenameUtils.getBaseName(originalFilename) + ".bpg";
		data.setVisibleOutputFilename(visible);
		data.getSteps().add(getClass().getSimpleName() + ".handleFileUpload");
		data.setLastOperation(instantService.now());
		return data;
	}

	public ProcessData saveOrUpdate(ProcessData data) {
		data.setLastOperation(instantService.now());

		database.put(data.getId(), data);
		return data;
	}
	
	public ProcessData get(String uuid) {
		return database.get(uuid);
	}

}
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.validator.GenericValidator;
import org.bellard.bpg.batch.SpringBatchRunner;
import org.bellard.bpg.batch.job.BpgGeneratorJob;
import org.bellard.bpg.batch.step.NotifyFailureStep;
import org.bellard.bpg.exception.BusinessException;
import org.bellard.bpg.exception.TechnicalException;
import org.bellard.bpg.model.AppSession;
import org.bellard.bpg.model.ExceptionData;
import org.bellard.bpg.model.ProcessData;
import org.bellard.bpg.model.UploadedFile;
import org.bellard.bpg.model.UploadedFiles;
import org.bellard.bpg.service.AppSessionService;
import org.bellard.bpg.service.FileResourceService;
import org.bellard.bpg.service.InstantService;
import org.bellard.bpg.service.ProcessDataService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.google.code.kaptcha.Constants;
/**
 * 
 */
package org.bellard.bpg.web;





/**
 * @author ebellard
 *
 */
@Controller
public class BpgController {

	private static final Logger logger = LoggerFactory.getLogger(BpgController.class);

	@Value("${spring.batch.enabled}")
	@Autowired
	private boolean springBatchEnabled;

	@Autowired
	private ProcessDataService processDataService;

	@Autowired
	private AppSessionService appSessionService;