/**
* OrbisGIS is a java GIS application dedicated to research in GIScience.
* OrbisGIS is developed by the GIS group of the DECIDE team of the
* Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>.
*
* The GIS group of the DECIDE team is located at :
*
* Laboratoire Lab-STICC – CNRS UMR 6285
* Equipe DECIDE
* UNIVERSITÉ DE BRETAGNE-SUD
* Institut Universitaire de Technologie de Vannes
* 8, Rue Montaigne - BP 561 56017 Vannes Cedex
*
* OrbisGIS is distributed under GPL 3 license.
*
* Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488)
* Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285)
*
* This file is part of OrbisGIS.
*
* OrbisGIS is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* OrbisGIS is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* OrbisGIS. If not, see <http://www.gnu.org/licenses/>.
*
* For more information, please consult: <http://www.orbisgis.org/>
* or contact directly:
* info_at_ orbisgis.org
*/
package org.orbisgis.toolboxeditor.utils;
import net.opengis.wps._2_0.*;
import org.orbiswps.server.execution.ProcessExecutionListener;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;
import javax.swing.Timer;
import javax.xml.datatype.XMLGregorianCalendar;
import java.awt.*;
import java.awt.event.ActionListener;
import java.beans.EventHandler;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;
/**
* This class represents the WPS server Job object but in the client side.
* The aim of the class is to follow the life cycle of a server side Job.
*
* When it is construct, the UUID of the server side Job is given and will be used later for refreshing the job status.
* Once created, some property listener can be add for listening the update of the Process state.
* Then, once configured, the result of the process execution (the StatusInfo object) is given with the setStatus()
* methods. The job will save the state, and fire two PropertyEvents : one with the new state of the execution and
* another with the execution log.
* After firering the event, with the refresh date of the StatusInfo object, a timer is launched and will wake up the
* Job. When the Job is woken up by the timer, it will fire a PropertyEvent with the request of the update of the job status.
* The class in charge of that (often the WpsClient) will get the new StatusInfo object form the Wps server thanks to the
* GetStatus request and give it back to the client side Job object. Then the status update is done, the timer is reset
* and so on.
* Once the status is 'succedded' or 'failed', the Job fire a PropertyEvent for getting the execution result from its
* listeners. The listener in charge (often the WpsClient) gives back the Result object from the WpsServer and the Job
* fire the last two status and log PropertyEvents.
*
* @author Sylvain PALOMINOS
*/
public class Job implements ProcessExecutionListener{
/** Static strings used for the PropertyEvents. */
public static final String STATE_PROPERTY = "STATE_PROPERTY";
public static final String LOG_PROPERTY = "LOG_PROPERTY";
public static final String CANCEL = "CANCEL";
public static final String REFRESH_STATUS = "REFRESH_STATUS";
public static final String GET_RESULTS = "GET_RESULTS";
public static final String PERCENT_COMPLETED_PROPERTY = "PERCENT_COMPLETED_PROPERTY";
public static final String ESTIMATED_COMPLETION_PROPERTY = "ESTIMATED_COMPLETION_PROPERTY";
/** I18N object */
private static final I18n I18N = I18nFactory.getI18n(ProcessExecutionListener.class);
/** Id of the server side Job. */
private UUID id;
/** Process state at the last refresh date. */
private ProcessExecutionListener.ProcessState state;
/** Time when the process has started. */
private Long startTime;
/** Map containing the logs of the execution of their display color. */
private Map<String, Color> logMap;
/** Process executed by the Job */
private ProcessDescriptionType process;
/** List of property listener which are waiting for any changes on the process*/
private List<PropertyChangeListener> propertyChangeListenerList;
/**
* Constructs a Job with the id of the server side Job and the ProcessDescriptionType of the process to execute.
* This way, while no PropertyListener is add, the Job will only update it's internal state without communicating
* it to other class.
*
* @param id UUID of the server side Job.
* @param process ProcessDescriptionType of the process to execute.
*/
public Job(UUID id, ProcessDescriptionType process){
this.logMap = new LinkedHashMap<>();
this.propertyChangeListenerList = new ArrayList<>();
this.process = process;
this.id = id;
}
/**
* Returns the UUID of the track server side Job.
* @return The UUID of the server side Job.
*/
public UUID getId() {
return id;
}
/**
* Returns the state of the track server side Job.
* @return The state of the server side Job.
*/
public ProcessExecutionListener.ProcessState getState() {
return state;
}
@Override
public void setProcessState(ProcessState processState) {
this.state = processState;
if (processState.equals(ProcessExecutionListener.ProcessState.FAILED)) {
appendLog(ProcessExecutionListener.LogType.ERROR, processState.toString());
} else {
appendLog(ProcessExecutionListener.LogType.INFO, processState.toString());
}
//If the process has ended with success, retrieve the results.
//The firing of the process state change will be done later
if (processState.equals(ProcessExecutionListener.ProcessState.SUCCEEDED)) {
firePropertyChangeEvent(new PropertyChangeEvent(this, GET_RESULTS, id, id));
}
//Else, fire the change of the process state.
else {
firePropertyChangeEvent(new PropertyChangeEvent(this, STATE_PROPERTY, null, processState));
}
}
public void setPercentCompleted(Integer percentCompleted) {
firePropertyChangeEvent(new PropertyChangeEvent(this, PERCENT_COMPLETED_PROPERTY, null, percentCompleted));
}
public void setEstimatedCompletion(XMLGregorianCalendar completionDate) {
long completionMillis = completionDate.toGregorianCalendar().getTime().getTime() - new Date().getTime();
firePropertyChangeEvent(new PropertyChangeEvent(this, ESTIMATED_COMPLETION_PROPERTY, null, completionMillis));
}
/**
* Returns the map of the Job execution logs.
* @return The map of the Job execution logs.
*/
public Map<String, Color> getLogMap() {
return logMap;
}
/**
* Put a log in the Job log map
* @param log String text of the log.
* @param color Color of the log text.
*/
public void putLog(String log, Color color) {
this.logMap.put(log, color);
}
@Override
public void setStartTime(long time) {
this.startTime = time + 60*60*1000;
}
@Override
public void appendLog(LogType logType, String message) {
Color color;
switch(logType){
case ERROR:
color = Color.RED;
break;
case WARN:
color = Color.ORANGE;
break;
case INFO:
default:
color = Color.BLACK;
}
Date date = new Date(System.currentTimeMillis() - startTime);
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
String log = timeFormat.format(date) + " : " + logType.name() + " : " + message + "";
putLog(log, color);
firePropertyChangeEvent(new PropertyChangeEvent(
this, LOG_PROPERTY, null, new AbstractMap.SimpleEntry<>(log, color)));
}
/**
* Start a timer with the date when the Job should ask again the process execution job status to the WpsService.
* @param date Date when the state should be asked.
*/
private void startRefreshTimer(XMLGregorianCalendar date){
if(date != null) {
long delta = date.toGregorianCalendar().getTime().getTime() - new Date().getTime();
if (delta <= 0) {
delta = 1;
}
Timer timer = new Timer((int) delta, EventHandler.create(ActionListener.class, this, "askStatusRefresh"));
timer.setRepeats(false);
timer.start();
}
}
/**
* Fire an event to ask the refreshing of the status.
*/
public void askStatusRefresh(){
PropertyChangeEvent event = new PropertyChangeEvent(this, REFRESH_STATUS, this.getId(), this.getId());
firePropertyChangeEvent(event);
}
/**
* Sets the new status of the Job with the Wps server answer object StatusInfo.
* @param statusInfo StatusInfo object coming from the Wps server.
*/
public void setStatus(StatusInfo statusInfo){
setProcessState(ProcessExecutionListener.ProcessState.valueOf(statusInfo.getStatus().toUpperCase()));
startRefreshTimer(statusInfo.getNextPoll());
setPercentCompleted(statusInfo.getPercentCompleted());
if(statusInfo.getEstimatedCompletion() != null) {
setEstimatedCompletion(statusInfo.getEstimatedCompletion());
}
}
/**
* Sets the Result of the process job.
* @param result Result object coming from the Wps server.
*/
public void setResult(Result result) {
appendLog(ProcessExecutionListener.LogType.INFO, "");
appendLog(ProcessExecutionListener.LogType.INFO, I18N.tr("Process result :"));
//For each output, try to build a human readable string containing the output which will be displayed in the log.
for(DataOutputType output : result.getOutput()){
Object o = output.getData().getContent().get(0);
for (OutputDescriptionType outputDescriptionType : process.getOutput()) {
if (outputDescriptionType.getIdentifier().getValue().equals(output.getId())) {
appendLog(ProcessExecutionListener.LogType.INFO,
outputDescriptionType.getTitle().get(0).getValue() + " = " + o.toString());
}
}
}
firePropertyChangeEvent(new PropertyChangeEvent(this, STATE_PROPERTY, null, getState()));
}
/**
* Returns the process executed by the server side Job.
* @return The process executed by the server side Job.
*/
public ProcessDescriptionType getProcess() {
return process;
}
/**
* Adds a PropertyChangeListener which will track the updates of the Job.
* @param listener Listeners track the Job.
*/
public void addPropertyChangeListener(PropertyChangeListener listener){
this.propertyChangeListenerList.add(listener);
}
/**
* Removes a PropertyChangeListener .
* @param listener Listeners to remove.
*/
public void removePropertyChangeListener(PropertyChangeListener listener){
this.propertyChangeListenerList.remove(listener);
}
/**
* Fires a PropertyEvent to all the registered PropertyListeners.
* @param event Event to transmit to all the listeners.
*/
public void firePropertyChangeEvent(PropertyChangeEvent event){
if(!propertyChangeListenerList.isEmpty()) {
for (PropertyChangeListener listener : propertyChangeListenerList) {
listener.propertyChange(event);
}
}
}
}