package tap;

/*
 * This file is part of TAPLibrary.
 * 
 * TAPLibrary is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * TAPLibrary 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Copyright 2012-2016 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 *                       Astronomisches Rechen Institut (ARI)
 */

import java.util.Date;
import java.util.List;

import tap.log.TAPLog;
import tap.parameters.DALIUpload;
import tap.parameters.TAPParameters;
import uws.UWSException;
import uws.job.ErrorSummary;
import uws.job.ExecutionPhase;
import uws.job.JobThread;
import uws.job.Result;
import uws.job.UWSJob;
import uws.job.parameters.UWSParameters;
import uws.job.user.JobOwner;
import uws.service.log.UWSLog.LogLevel;

/**
 * <p>Description of a TAP job. This class is used for asynchronous but also synchronous queries.</p>
 * 
 * <p>
 * 	On the contrary to {@link UWSJob}, it is loading parameters from {@link TAPParameters} instances rather than {@link UWSParameters}.
 * 	However, {@link TAPParameters} is an extension of {@link UWSParameters}. That's what allow the UWS library to use both {@link TAPJob} and {@link TAPParameters}.
 * </p>
 * 
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 2.1 (01/2016)
 */
public class TAPJob extends UWSJob {
	private static final long serialVersionUID = 1L;

	/** Name of the standard TAP parameter which specifies the type of request to execute: "REQUEST". */
	public static final String PARAM_REQUEST = "request";
	/** REQUEST value meaning an ADQL query must be executed: "doQuery". */
	public static final String REQUEST_DO_QUERY = "doQuery";
	/** REQUEST value meaning VO service capabilities must be returned: "getCapabilities". */
	public static final String REQUEST_GET_CAPABILITIES = "getCapabilities";

	/** Name of the standard TAP parameter which specifies the query language: "LANG". <i>(only the ADQL language is supported by default in this version of the library)</i> */
	public static final String PARAM_LANGUAGE = "lang";
	/** LANG value meaning ADQL language: "ADQL". */
	public static final String LANG_ADQL = "ADQL";
	/** LANG value meaning PQL language: "PQL". <i>(this language is not supported in this version of the library)</i> */
	public static final String LANG_PQL = "PQL";

	/** Name of the standard TAP parameter which specifies the version of the TAP protocol that must be used: "VERSION". <i>(only the version 1.0 is supported in this version of the library)</i> */
	public static final String PARAM_VERSION = "version";
	/** VERSION value meaning the version 1.0 of TAP: "1.0". */
	public static final String VERSION_1_0 = "1.0";

	/** Name of the standard TAP parameter which specifies the output format (format of a query result): "FORMAT". */
	public static final String PARAM_FORMAT = "format";
	/** FORMAT value meaning the VOTable format: "votable". */
	public static final String FORMAT_VOTABLE = "votable";

	/** Name of the standard TAP parameter which specifies the maximum number of rows that must be returned in the query result: "MAXREC". */
	public static final String PARAM_MAX_REC = "maxRec";
	/** Special MAXREC value meaning the number of output rows is not limited. */
	public static final int UNLIMITED_MAX_REC = -1;

	/** Name of the standard TAP parameter which specifies the query to execute: "QUERY". */
	public static final String PARAM_QUERY = "query";

	/** Name of the standard TAP parameter which defines the tables to upload in the database for the query execution: "UPLOAD". */
	public static final String PARAM_UPLOAD = "upload";

	/** Name of the library parameter which informs about a query execution progression: "PROGRESSION". <i>(this parameter is removed once the execution is finished)</i> */
	public static final String PARAM_PROGRESSION = "progression";

	/** Internal query execution report. */
	protected TAPExecutionReport execReport = null;

	/** Parameters of this job for its execution. */
	protected final TAPParameters tapParams;

	/**
	 * <p>Build a pending TAP job with the given parameters.</p>
	 * 
	 * <p><i><u>Note:</u> if the parameter {@link #PARAM_PHASE} (</i>phase<i>) is given with the value {@link #PHASE_RUN}
	 * the job execution starts immediately after the job has been added to a job list or after {@link #applyPhaseParam(JobOwner)} is called.</i></p>
	 * 
	 * @param owner		User who owns this job. <i>MAY BE NULL</i>
	 * @param tapParams	Set of parameters.
	 * 
	 * @throws TAPException	If one of the given parameters has a forbidden or wrong value.
	 */
	public TAPJob(final JobOwner owner, final TAPParameters tapParams) throws TAPException{
		super(owner, tapParams);
		this.tapParams = tapParams;
		tapParams.check();
	}

	/**
	 * <p>Build a pending TAP job with the given parameters.
	 * The given HTTP request ID will be used as Job ID if not already used by another job.</p>
	 * 
	 * <p><i><u>Note:</u> if the parameter {@link #PARAM_PHASE} (</i>phase<i>) is given with the value {@link #PHASE_RUN}
	 * the job execution starts immediately after the job has been added to a job list or after {@link #applyPhaseParam(JobOwner)} is called.</i></p>
	 * 
	 * @param owner		User who owns this job. <i>MAY BE NULL</i>
	 * @param tapParams	Set of parameters.
	 * @param requestID	ID of the HTTP request which has initiated the creation of this job.
	 *                 	<i>Note: if NULL, empty or already used, a job ID will be generated thanks to {@link #generateJobId()}.</i>
	 * 
	 * @throws TAPException	If one of the given parameters has a forbidden or wrong value.
	 * 
	 * @since 2.1
	 */
	public TAPJob(final JobOwner owner, final TAPParameters tapParams, final String requestID) throws TAPException{
		super(owner, tapParams, requestID);
		this.tapParams = tapParams;
		tapParams.check();
	}

	/**
	 * <p>Restore a job in a state defined by the given parameters.
	 * The phase must be set separately with {@link #setPhase(uws.job.ExecutionPhase, boolean)}, where the second parameter is true.</p>
	 * 
	 * @param jobID		ID of the job.
	 * @param owner		User who owns this job.
	 * @param params	Set of not-standard UWS parameters (i.e. what is called by {@link UWSJob} as additional parameters ; they includes all TAP parameters).
	 * @param quote		Quote of this job.
	 * @param startTime	Date/Time at which this job started. <i>(if not null, it means the job execution was finished, so a endTime should be provided)</i>
	 * @param endTime	Date/Time at which this job finished.
	 * @param results	List of results. <i>NULL if the job has not been executed, has been aborted or finished with an error.</i>
	 * @param error		Error with which this job ends.
	 * 
	 * @throws TAPException	If one of the given parameters has a forbidden or wrong value.
	 */
	public TAPJob(final String jobID, final JobOwner owner, final TAPParameters params, final long quote, final long startTime, final long endTime, final List<Result> results, final ErrorSummary error) throws TAPException{
		super(jobID, owner, params, quote, startTime, endTime, results, error);
		this.tapParams = params;
		this.tapParams.check();
	}

	/**
	 * Get the object storing and managing the set of all (UWS and TAP) parameters.
	 * 
	 * @return The object managing all job parameters.
	 */
	public final TAPParameters getTapParams(){
		return tapParams;
	}

	/**
	 * <p>Get the value of the REQUEST parameter.</p>
	 * 
	 * <p>This value must be {@value #REQUEST_DO_QUERY}.</p>
	 * 
	 * @return	REQUEST value.
	 */
	public final String getRequest(){
		return tapParams.getRequest();
	}

	/**
	 * Get the value of the FORMAT parameter.
	 * 
	 * @return	FORMAT value.
	 */
	public final String getFormat(){
		return tapParams.getFormat();
	}

	/**
	 * <p>Get the value of the LANG parameter.</p>
	 * 
	 * <p>This value should always be {@value #LANG_ADQL} in this version of the library</p>
	 * 
	 * @return	LANG value.
	 */
	public final String getLanguage(){
		return tapParams.getLang();
	}

	/**
	 * <p>Get the value of the MAXREC parameter.</p>
	 * 
	 * <p>If this value is negative, it means the number of output rows is not limited.</p>
	 * 
	 * @return	MAXREC value.
	 */
	public final int getMaxRec(){
		return tapParams.getMaxRec();
	}

	/**
	 * Get the value of the QUERY parameter (i.e. the query, in the language returned by {@link #getLanguage()}, to execute).
	 * 
	 * @return	QUERY value.
	 */
	public final String getQuery(){
		return tapParams.getQuery();
	}

	/**
	 * <p>Get the value of the VERSION parameter.</p>
	 * 
	 * <p>This value should be {@value #VERSION_1_0} in this version of the library.</p>
	 * 
	 * @return	VERSION value.
	 */
	public final String getVersion(){
		return tapParams.getVersion();
	}

	/**
	 * <p>Get the value of the UPLOAD parameter.</p>
	 * 
	 * <p>This value must be formatted as specified by the TAP standard (= a semicolon separated list of DALI uploads).</p>
	 * 
	 * @return	UPLOAD value.
	 */
	public final String getUpload(){
		return tapParams.getUpload();
	}

	/**
	 * <p>Get the list of tables to upload in the database for the query execution.</p>
	 * 
	 * <p>The returned array is an interpretation of the UPLOAD parameter.</p>
	 * 
	 * @return	List of tables to upload.
	 */
	public final DALIUpload[] getTablesToUpload(){
		return tapParams.getUploadedTables();
	}

	/**
	 * <p>Get the execution report.</p>
	 * 
	 * <p>
	 * 	This report is available only during or after the job execution.
	 * 	It tells in which step the execution is, and how long was the previous steps.
	 * 	It can also give more information about the number of resulting rows and columns.
	 * </p>
	 * 
	 * @return The execReport.
	 */
	public final TAPExecutionReport getExecReport(){
		return execReport;
	}

	/**
	 * <p>Set the execution report.</p>
	 * 
	 * <p><b>IMPORTANT:
	 * 	This function can be called only if the job is running or is being restored, otherwise an exception would be thrown.
	 * 	It should not be used by implementors, but only by the internal library processing.
	 * </b></p>
	 * 
	 * @param execReport	An execution report.
	 * 
	 * @throws UWSException	If this job has never been restored and is not running.
	 */
	public final void setExecReport(final TAPExecutionReport execReport) throws UWSException{
		if (getRestorationDate() == null && (thread == null || thread.isFinished()))
			throw new UWSException("Impossible to set an execution report if the job is not in the EXECUTING phase ! Here, the job \"" + jobId + "\" is in the phase " + getPhase());
		this.execReport = execReport;
	}

	/**
	 * <p>Create the thread to use for the execution of this job.</p>
	 * 
	 * <p><i>Note: If the job already exists, this function does nothing.</i></p>
	 * 
	 * @throws NullPointerException	If the factory returned NULL rather than the asked {@link JobThread}.
	 * @throws UWSException			If the thread creation fails.
	 * 
	 * @see TAPFactory#createJobThread(UWSJob)
	 * 
	 * @since 2.0
	 */
	private final void createThread() throws NullPointerException, UWSException{
		if (thread == null){
			thread = getFactory().createJobThread(this);
			if (thread == null)
				throw new NullPointerException("Missing job work! The thread created by the factory is NULL => The job can't be executed!");
		}
	}

	/**
	 * <p>Check whether this job is able to start right now.</p>
	 * 
	 * <p>
	 * 	Basically, this function try to get a database connection. If none is available,
	 * 	then this job can not start and this function return FALSE. In all the other cases,
	 * 	TRUE is returned.
	 * </p>
	 * 
	 * <p><b>Warning:</b> This function will indirectly open and keep a database connection, so that the job can be started just after its call.
	 * If it turns out that the execution won't start just after this call, the DB connection should be closed in some way in order to save database resources.</i></p>
	 * 
	 * @return	<i>true</i> if this job can start right now, <i>false</i> otherwise.
	 * 
	 * @since 2.0
	 */
	public final boolean isReadyForExecution(){
		return thread != null && ((AsyncThread)thread).isReadyForExecution();
	}

	@Override
	public final void start(final boolean useManager) throws UWSException{
		// This job must know its jobs list and this jobs list must know its UWS:
		if (getJobList() == null || getJobList().getUWS() == null)
			throw new IllegalStateException("A TAPJob can not start if it is not linked to a job list or if its job list is not linked to a UWS.");

		// If already running do nothing:
		else if (isRunning())
			return;

		// If asked propagate this request to the execution manager:
		else if (useManager){
			// Create its corresponding thread, if not already existing:
			createThread();
			// Ask to the execution manager to test whether the job is ready for execution, and if, execute it (by calling this function with "false" as parameter):
			getJobList().getExecutionManager().execute(this);

		}// Otherwise start directly the execution:
		else{
			// Create its corresponding thread, if not already existing:
			createThread();
			if (!isReadyForExecution()){
				UWSException ue = new NoDBConnectionAvailableException();
				((TAPLog)getLogger()).logDB(LogLevel.ERROR, null, "CONNECTION_LACK", "No more database connection available for the moment!", ue);
				getLogger().logJob(LogLevel.ERROR, this, "ERROR", "Asynchronous job " + jobId + " execution aborted: no database connection available!", null);
				throw ue;
			}

			// Change the job phase:
			setPhase(ExecutionPhase.EXECUTING);

			// Set the start time:
			setStartTime(new Date());

			// Run the job:
			thread.start();
			(new JobTimeOut()).start();

			// Log the start of this job:
			getLogger().logJob(LogLevel.INFO, this, "START", "Job \"" + jobId + "\" started.", null);
		}
	}

	/** @since 2.1 */
	@Override
	protected void stop(){
		if (!isStopped()){
			synchronized(thread){
				stopping = true;

				// Interrupts the thread:
				thread.interrupt();

				// Cancel the query execution if any currently running:
				((AsyncThread)thread).executor.cancelQuery();

				// Wait a little for its end:
				if (waitForStop > 0){
					try{
						thread.join(waitForStop);
					}catch(InterruptedException ie){
						getLogger().logJob(LogLevel.WARNING, this, "END", "Unexpected InterruptedException while waiting for the end of the execution of the job \"" + jobId + "\" (thread ID: " + thread.getId() + ")!", ie);
					}
				}
			}
		}
	}

	/**
	 * This exception is thrown by a job execution when no database connection are available anymore.
	 * 
	 * @author Gr&eacute;gory Mantelet (ARI)
	 * @version 2.0 (02/2015)
	 * @since 2.0
	 */
	public static class NoDBConnectionAvailableException extends UWSException {
		private static final long serialVersionUID = 1L;

		public NoDBConnectionAvailableException(){
			super("Service momentarily too busy! Please try again later.");
		}

	}

}