content: /**
   * ﻿Copyright (C) 2007 - 2019 52°North Initiative for Geospatial Open Source
   * Software GmbH
   *
   * This program is free software; you can redistribute it and/or modify it
   * under the terms of the GNU General Public License version 2 as published
   * by the Free Software Foundation.
   *
   * If the program is linked with libraries which are licensed under one of
   * the following licenses, the combination of the program with the linked
   * library is not considered a "derivative work" of the program:
   *
   *       • Apache License, version 2.0
   *       • Apache Software License, version 1.0
   *       • GNU Lesser General Public License, version 3
   *       • Mozilla Public License, versions 1.0, 1.1 and 2.0
   *       • Common Development and Distribution License (CDDL), version 1.0
   *
   * Therefore the distribution of the program linked with libraries licensed
   * under the aforementioned licenses, is permitted by the copyright holders
   * if the distribution is compliant with both the GNU General Public
   * License version 2 and the aforementioned licenses.
   *
   * This program 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.
   */
  package org.n52.wps.server.handler;
  
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.Map;
  import java.util.concurrent.ExecutionException;
  import java.util.concurrent.RejectedExecutionException;
  
  import javax.xml.parsers.DocumentBuilderFactory;
  import javax.xml.parsers.ParserConfigurationException;
  
  import org.apache.commons.collections.map.CaseInsensitiveMap;
  import org.apache.commons.io.IOUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.n52.wps.server.ExceptionReport;
  import org.n52.wps.server.WebProcessingService;
  import org.n52.wps.server.request.CapabilitiesRequest;
  import org.n52.wps.server.request.DescribeProcessRequest;
  import org.n52.wps.server.request.ExecuteRequest;
  import org.n52.wps.server.request.Request;
  import org.n52.wps.server.request.RetrieveResultRequest;
  import org.n52.wps.server.response.ExecuteResponse;
  import org.n52.wps.server.response.Response;
  import org.w3c.dom.Document;
  import org.w3c.dom.Node;
  import org.xml.sax.SAXException;
  
  /**
   * This class accepts client requests, determines its type and then schedules
   * the {@link ExecuteRequest}'s for execution. The request is executed for a
   * short time, within the client will be served with an immediate result. If the
   * time runs out, the client will be served with a reference to the future
   * result. The client can come back later to retrieve the result. Uses
   * "computation_timeout_seconds" from wps.properties
   * 
   * @author Timon ter Braak
   */
  public class RequestHandler {
  	
      public static final String VERSION_ATTRIBUTE_NAME = "version";
  
  	/** Computation timeout in seconds */
  	protected static RequestExecutor pool = new RequestExecutor();
  
  	protected OutputStream os;
  
  	private static Logger LOGGER = LoggerFactory.getLogger(RequestHandler.class);
  	
  	protected String responseMimeType;
  	
  	protected Request req;
  	
  	// Empty constructor due to classes which extend the RequestHandler
  	protected RequestHandler() {
  		
  	}
  
  	/**
  	 * Handles requests of type HTTP_GET (currently capabilities and
  	 * describeProcess). A Map is used to represent the client input.
  	 * 
  	 * @param params
  	 *            The client input
  	 * @param os
  	 *            The OutputStream to write the response to.
  	 * @throws ExceptionReport
  	 *             If the requested operation is not supported
  	 */
  	public RequestHandler(Map<String, String[]> params, OutputStream os)
  			throws ExceptionReport {
  		this.os = os;
  		//sleepingTime is 0, by default.
  		/*if(WPSConfiguration.getInstance().exists(PROPERTY_NAME_COMPUTATION_TIMEOUT)) {
  			this.sleepingTime = Integer.parseInt(WPSConfiguration.getInstance().getProperty(PROPERTY_NAME_COMPUTATION_TIMEOUT));
  		}
  		String sleepTime = WPSConfig.getInstance().getWPSConfig().getServer().getComputationTimeoutMilliSeconds();
  		*/
  		
  		
  		Request req;
  		CaseInsensitiveMap ciMap = new CaseInsensitiveMap(params);
  		
  		/*
  		 * check if service parameter is present and equals "WPS"
  		 * otherwise an ExceptionReport will be thrown
  		 */
  		String serviceType = Request.getMapValue("service", ciMap, true);
  		
  		if(!serviceType.equalsIgnoreCase("WPS")){
  			throw new ExceptionReport("Parameter <service> is not correct, expected: WPS, got: " + serviceType, 
  					ExceptionReport.INVALID_PARAMETER_VALUE, "service");
  		}
  
  		/*
  		 * check language. if not supported, return ExceptionReport
  		 * Fix for https://bugzilla.52north.org/show_bug.cgi?id=905
  		 */
  		String language = Request.getMapValue("language", ciMap, false);
  		
  		if(language != null){
  			Request.checkLanguageSupported(language);
  		}
  
  		// get the request type
  		String requestType = Request.getMapValue("request", ciMap, true);
  		
  		if (requestType.equalsIgnoreCase("GetCapabilities")) {
  			req = new CapabilitiesRequest(ciMap);
  		} 
  		else if (requestType.equalsIgnoreCase("DescribeProcess")) {
  			req = new DescribeProcessRequest(ciMap);
  		}
  		else if (requestType.equalsIgnoreCase("Execute")) {
  			req = new ExecuteRequest(ciMap);
  			setResponseMimeType((ExecuteRequest)req);
  		} 
  		else if (requestType.equalsIgnoreCase("RetrieveResult")) {
  			req = new RetrieveResultRequest(ciMap);
  		} 
  		else {
  			throw new ExceptionReport(
  					"The requested Operation is not supported or not applicable to the specification: "
  							+ requestType,
  					ExceptionReport.OPERATION_NOT_SUPPORTED, requestType);
  		}
  
  		this.req = req;
  	}
  
  	/**
  	 * Handles requests of type HTTP_POST (currently executeProcess). A Document
  	 * is used to represent the client input. This Document must first be parsed
  	 * from an InputStream.
  	 * 
  	 * @param is
  	 *            The client input
  	 * @param os
  	 *            The OutputStream to write the response to.
  	 * @throws ExceptionReport
  	 */
  	public RequestHandler(InputStream is, OutputStream os)
  			throws ExceptionReport {
  		String nodeName, localName, nodeURI, version = null;
  		Document doc;
  		this.os = os;
  		
  		boolean isCapabilitiesNode = false;
  		
  		try {
  			System.setProperty("javax.xml.parsers.DocumentBuilderFactory", "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
  		
  			DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
  			fac.setNamespaceAware(true);
  
  			// parse the InputStream to create a Document
  			doc = fac.newDocumentBuilder().parse(is);
  			
  			// Get the first non-comment child.
  			Node child = doc.getFirstChild();
  			while(child.getNodeName().compareTo("#comment")==0) {
  				child = child.getNextSibling();
  			}
  			nodeName = child.getNodeName();
  			localName = child.getLocalName();
  			nodeURI = child.getNamespaceURI();
  			Node versionNode = child.getAttributes().getNamedItem("version");
  			
  			/*
  			 * check for service parameter. this has to be present for all requests
  			 */
  			Node serviceNode = child.getAttributes().getNamedItem("service");
  			
  			if(serviceNode == null){
  				throw new ExceptionReport("Parameter <service> not specified.", ExceptionReport.MISSING_PARAMETER_VALUE, "service");
  			}else{
  				if(!serviceNode.getNodeValue().equalsIgnoreCase("WPS")){
  					throw new ExceptionReport("Parameter <service> not specified.", ExceptionReport.INVALID_PARAMETER_VALUE, "service");
  				}
  			}
  			
              isCapabilitiesNode = nodeName.toLowerCase().contains("capabilities");
  			if(versionNode == null && !isCapabilitiesNode) {
  				throw new ExceptionReport("Parameter <version> not specified.", ExceptionReport.MISSING_PARAMETER_VALUE, "version");
  			}
  			//TODO: I think this can be removed, as capabilities requests do not have a version parameter (BenjaminPross)
  			if(!isCapabilitiesNode){
  //				version = child.getFirstChild().getTextContent();//.getNextSibling().getFirstChild().getNextSibling().getFirstChild().getNodeValue();
  				version = child.getAttributes().getNamedItem("version").getNodeValue();
  			}
  			/*
  			 * check language, if not supported, return ExceptionReport
  			 * Fix for https://bugzilla.52north.org/show_bug.cgi?id=905
  			 */
  			Node languageNode = child.getAttributes().getNamedItem("language");
  			if(languageNode != null){
  				String language = languageNode.getNodeValue();
  				Request.checkLanguageSupported(language);
  			}
  		} catch (SAXException e) {
  			throw new ExceptionReport(
  					"There went something wrong with parsing the POST data: "
  							+ e.getMessage(),
  					ExceptionReport.NO_APPLICABLE_CODE, e);
  		} catch (IOException e) {
  			throw new ExceptionReport(
  					"There went something wrong with the network connection.",
  					ExceptionReport.NO_APPLICABLE_CODE, e);
  		} catch (ParserConfigurationException e) {
  			throw new ExceptionReport(
  					"There is a internal parser configuration error",
  					ExceptionReport.NO_APPLICABLE_CODE, e);
  		}
  		//Fix for Bug 904 https://bugzilla.52north.org/show_bug.cgi?id=904
  		if(!isCapabilitiesNode && version == null) {
  			throw new ExceptionReport("Parameter <version> not specified." , ExceptionReport.MISSING_PARAMETER_VALUE, "version");
  		}
  		if(!isCapabilitiesNode && !version.equals(Request.SUPPORTED_VERSION)) {
  			throw new ExceptionReport("Version not supported." , ExceptionReport.INVALID_PARAMETER_VALUE, "version");
  		}
  		// get the request type
  		if (nodeURI.equals(WebProcessingService.WPS_NAMESPACE) && localName.equals("Execute")) {
  			req = new ExecuteRequest(doc);
  			setResponseMimeType((ExecuteRequest)req);
  		}else if (nodeURI.equals(WebProcessingService.WPS_NAMESPACE) && localName.equals("GetCapabilities")){
  			req = new CapabilitiesRequest(doc);
  			this.responseMimeType = "text/xml";
  		} else if (nodeURI.equals(WebProcessingService.WPS_NAMESPACE) && localName.equals("DescribeProcess")) {
  			req = new DescribeProcessRequest(doc);
  			this.responseMimeType = "text/xml";
  			
  		}  else if(!localName.equals("Execute")){
  			throw new ExceptionReport("The requested Operation not supported or not applicable to the specification: "
  					+ nodeName, ExceptionReport.OPERATION_NOT_SUPPORTED, localName);
  		}
  		else if(nodeURI.equals(WebProcessingService.WPS_NAMESPACE)) {
  			throw new ExceptionReport("specified namespace is not supported: "
  					+ nodeURI, ExceptionReport.INVALID_PARAMETER_VALUE);
  		}
  	}
  
  	/**
  	 * Handle a request after its type is determined. The request is scheduled
  	 * for execution. If the server has enough free resources, the client will
  	 * be served immediately. If time runs out, the client will be asked to come
  	 * back later with a reference to the result.
  	 * 
  	 * @param req The request of the client.
  	 * @throws ExceptionReport
  	 */
  	public void handle() throws ExceptionReport {
  		Response resp = null;
  		if(req ==null){
  			throw new ExceptionReport("Internal Error","");
  		}
  		if (req instanceof ExecuteRequest) {
  			// cast the request to an executerequest
  			ExecuteRequest execReq = (ExecuteRequest) req;
  			
  			execReq.updateStatusAccepted();
  			
  			ExceptionReport exceptionReport = null;
  			try {
  				if (execReq.isStoreResponse()) {
  					resp = new ExecuteResponse(execReq);
  					InputStream is = resp.getAsStream();
  					IOUtils.copy(is, os);
  					is.close();
                      pool.submit(execReq);
  					return;
  				}
  				try {
  					// retrieve status with timeout enabled
  					try {
  						resp = pool.submit(execReq).get();
  					}
  					catch (ExecutionException ee) {
  						LOGGER.warn("exception while handling ExecuteRequest.");
  						// the computation threw an error
  						// probably the client input is not valid
  						if (ee.getCause() instanceof ExceptionReport) {
  							exceptionReport = (ExceptionReport) ee
  									.getCause();
  						} else {
  							exceptionReport = new ExceptionReport(
  									"An error occurred in the computation: "
  											+ ee.getMessage(),
  									ExceptionReport.NO_APPLICABLE_CODE);
  						}
  					} catch (InterruptedException ie) {
  						LOGGER.warn("interrupted while handling ExecuteRequest.");
  						// interrupted while waiting in the queue
  						exceptionReport = new ExceptionReport(
  								"The computation in the process was interrupted.",
  								ExceptionReport.NO_APPLICABLE_CODE);
  					}
  				} finally {
  					if (exceptionReport != null) {
  						LOGGER.debug("ExceptionReport not null: " + exceptionReport.getMessage());
  						// NOT SURE, if this exceptionReport is also written to the DB, if required... test please!
  						throw exceptionReport;
  					}
  					// send the result to the outputstream of the client.
  				/*	if(((ExecuteRequest) req).isQuickStatus()) {
  						resp = new ExecuteResponse(execReq);
  					}*/
  					else if(resp == null) {
  						LOGGER.warn("null response handling ExecuteRequest.");
  						throw new ExceptionReport("Problem with handling threads in RequestHandler", ExceptionReport.NO_APPLICABLE_CODE);
  					}
  					if(!execReq.isStoreResponse()) {
  						InputStream is = resp.getAsStream();
  						IOUtils.copy(is, os);
  						is.close();
  						LOGGER.info("Served ExecuteRequest.");
  					}
  				}
  			} catch (RejectedExecutionException ree) {
                  LOGGER.warn("exception handling ExecuteRequest.", ree);
  				// server too busy?
  				throw new ExceptionReport(
  						"The requested process was rejected. Maybe the server is flooded with requests.",
  						ExceptionReport.SERVER_BUSY);
  			} catch (Exception e) {
                  LOGGER.error("exception handling ExecuteRequest.", e);
                  if (e instanceof ExceptionReport) {
                      throw (ExceptionReport)e;
                  }
                  throw new ExceptionReport("Could not read from response stream.", ExceptionReport.NO_APPLICABLE_CODE);
  			}
  		} else {
  			// for GetCapabilities and DescribeProcess:
  			resp = req.call();
  			try {
  				InputStream is = resp.getAsStream();
  				IOUtils.copy(is, os);
  				is.close();
  			} catch (IOException e) {
  				throw new ExceptionReport("Could not read from response stream.", ExceptionReport.NO_APPLICABLE_CODE);
  			}
  			
  		}
  	}
  	
  	protected void setResponseMimeType(ExecuteRequest req) {
  		if(req.isRawData()){
  			responseMimeType = req.getExecuteResponseBuilder().getMimeType();
  		}else{
  			responseMimeType = "text/xml";
  		}
  		
  		
  	}
  	
  	
  
  	public String getResponseMimeType(){
  		if(responseMimeType == null){
  			return "text/xml";
  		}
  		return responseMimeType.toLowerCase();
  	}
  	
  	
  }
  
  
  
  