ServiceCall.java [src/java/crp/utils] Revision:   Date:
/*
 * $Id$
 *
 * This file is part of the Cloud Services Integration Platform (CSIP),
 * a Model-as-a-Service framework, API, and application suite.
 *
 * 2012-2017, OMSLab, Colorado State University.
 *
 * OMSLab licenses this file to you under the MIT license.
 * See the LICENSE file in the project root for more information.
 */
package crp.utils;

import csip.ServiceException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

/**
 *
 * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
 */
abstract public class ServiceCall extends csip.Client implements Callable<String> {

  protected JSONObject request;
  protected JSONObject results = null;
  protected JSONObject requestMetainfoObject = null;
  protected JSONObject resultMetaInfoObject = null;
  protected JSONArray callResultSection = null;
  protected String metaError = "";
  protected String URI;
  protected String errorPrefix = "";
  protected boolean asyncCall = false;
  protected boolean finished = false;
  protected String suid = "";
  protected String status = "";
  private String path;
  private String context;
  private String pollURI;
  private String cancelURI;
  protected JSONObject requestJSON = null;
  protected CheckedServiceResult<ServiceCallData> serviceCallback = null;
  protected ServiceCallData serviceCallData = null;

  public ServiceCall(String URI) {
    super();
    this.URI = URI;
    breakoutURI();
  }

  public ServiceCall(String URI, Object data, CheckedServiceResult<ServiceCallData> serviceCallback) {
    super();
    this.URI = URI;
    this.serviceCallback = serviceCallback;
    serviceCallData = new ServiceCallData(data, this);
    breakoutURI();
  }

  public ServiceCall(String URI, CheckedServiceResult<ServiceCallData> serviceCallback) {
    super();
    this.URI = URI;
    this.serviceCallback = serviceCallback;
    breakoutURI();
  }

  public void setCallData(Object data) {
    synchronized (this) {
      this.serviceCallData = new ServiceCallData(data, this);;
    }
  }

  public Object getCallData() {
    return serviceCallData;
  }

  public String call() throws ServiceException {
    execPost();
    if (null != serviceCallback) {
      serviceCallback.accept(serviceCallData);
    }
    return status;
  }

  public JSONObject requestJSON() {
    return requestJSON;
  }

  public void cancel() throws ServiceException {
    if ((null == suid) || (suid.isEmpty())) {
      throwServiceCallException("Error attempting to cancel asynchronous call status.  No SUID is present in this class to check for with remote endpoint.");
    }
    //  TODO Poll service endpoit with suid
    if ((null != context) && (!context.isEmpty())) {
      try {
        results = new JSONObject(super.doGET(cancelURI + suid));
        String unknownSUID = results.optString("error");
        if (!unknownSUID.isEmpty()) {
          if (unknownSUID.contains("suid unknown")) {
            throwServiceCallException("The SUID for this service call are no longer available for retrieval.  Cannot cancel it, it may be already finished and archived by the time the cancel operation was attempted.");
          }
        }
      } catch (Exception ex) {
        throwServiceCallException("Results from cancelling the service call were not JSON.  Cannot check on results for suid: " + suid + ", Error: " + ex.getMessage(), ex);
      }

    } else {
      throwServiceCallException("Cannot cancel on this suid, " + suid + ", because there was no context location found in the URI string.");
    }
  }

  protected void parseResults() throws ServiceException {
    //   Error checking has already been done on these results.
    // If we get to this function, no further error
    // checking is necessary, except to look at the "results" array in the output, if desired.
    if (null == callResultSection) {
      throwServiceCallException("Cannot find results in the call to " + this.URI + " .");
    }
  }

  protected void parseAsyncResult() throws ServiceException {
    //   Error checking has already been done on these results.
    // If we get to this function, no further error
    // checking is necessary, except to look at the "results" array in the output, if desired.

    //  No results section is returned on a status of "Running".
//    if (null == callResultSection){
//      throwServiceCallException("Cannot find results in the call to " + this.URI + " .");
//    }
    //  When checkErrors() is called, the status is saved and finished is set.
//    if (finished) {
//      parseResults();
//    } else {
    if (!status.equalsIgnoreCase("running") && !status.equalsIgnoreCase("submitted")) {
      throwServiceCallException("Error encountered checking status of remote service call for suid: " + suid + ".  Metainfo status value was neither Finished nor Running, and no error message was found.");
    }
    //}

  }

  abstract protected void createRequest() throws ServiceException;

  public boolean isFinished() {
    return finished;
  }

  /**
   * Polls the service endpoint for an asynchronous call. If the service failed,
   * this call will throw an exception with the service failure results
   * specified.
   *
   * @return Returns true on finished and false on still pending results.
   */
  public boolean asyncDone() throws ServiceException {

    if ((null == suid) || (suid.isEmpty())) {
      throwServiceCallException("Error attempting to poll asynchronous call status.  No SUID is present in this class to check for with remote endpoint.");
    }
    //  TODO Poll service endpoit with suid
    if ((null != context) && (!context.isEmpty())) {
      try {
        results = new JSONObject(super.doGET(pollURI + suid));
        String unknownSUID = results.optString("error");
        if (!unknownSUID.isEmpty()) {
          if (unknownSUID.contains("suid unknown")) {
            throwServiceCallException("The results for this service call are no longer available for retrieval.  Please increase the TTL value for that service's session data.");
          }
        }
        checkErrors();
        if (finished) {
          parseResults();
        }
      } catch (Exception ex) {
        throwServiceCallException("Results from polling the service endpoint were not JSON.  Cannot check on results for suid: " + suid + ", Error: " + ex.getMessage(), ex);
      }

    } else {
      throwServiceCallException("Cannot check on this suid, " + suid + ", because there was no context location found in the URI string.");
    }

    return finished;
  }

  public String getMetaError() {
    return metaError;
  }

  public JSONObject getRequest() {
    return request;
  }

  public JSONObject getResults() {
    return results;
  }

  public JSONObject getReturnMetainfo() {
    return resultMetaInfoObject;
  }

  public JSONArray getResultSection() {
    return callResultSection;
  }

  public void setAsync(boolean value) {
    asyncCall = value;
  }

  public void setRequest(String request) throws JSONException, ServiceException {
    if ((null != request) && (!request.isEmpty())) {
      this.request = new JSONObject(request);
    } else {
      throwServiceCallException("Attempt to set request data with empty string.");
    }
  }

  public void setRequest(JSONObject request) throws ServiceException {
    if ((null != request) && (request.length() > 0)) {
      this.request = request;
    } else {
      throwServiceCallException("Attempt to set request data with empty JSONObject.");
    }
  }

  public void setURI(String URI) throws ServiceException {
    if ((null != URI) && (!URI.isEmpty())) {
      this.URI = URI;
      breakoutURI();
    } else {
      throwServiceCallException("Attempt to set URI with empty string.");
    }
  }

  public String getSUID() {
    return suid;
  }

  private void breakoutURI() {
    try {
      URL tURL = new URL(URI);
      path = tURL.getPath();

      String[] tokens = path.split("/");

      if ((null != tokens) && (tokens.length > 2)) {
        context = tokens[1];
        pollURI = tURL.getProtocol() + "://" + tURL.getHost() + ":" + tURL.getPort() + "/" + context + "/q/";
        cancelURI = tURL.getProtocol() + "://" + tURL.getHost() + ":" + tURL.getPort() + "/" + context + "/cancel/";
      } else {
        context = "";
      }

    } catch (MalformedURLException ex) {
      Logger.getLogger(ServiceCall.class.getName()).log(Level.SEVERE, "Error trying to get path and context from URI String.", ex);
    }
  }

  private void checkErrors() throws ServiceException {
    try {
      if ((null == results) || (results.length() <= 0)) {
        throwServiceCallException("No data was returned from " + URI + " for this request. ");
      }

      resultMetaInfoObject = results.getJSONObject("metainfo");
      status = resultMetaInfoObject.optString("status");

      if ((null != status) && (!status.isEmpty())) {
        if (status.equalsIgnoreCase("finished")) {
          finished = true;
        }
        suid = resultMetaInfoObject.optString("suid");
        if ((null == suid) || suid.isEmpty()) {
          throwServiceCallException("No SUID provided by csip service callled.");
        }
      } else {
        throwServiceCallException("No status value was found in the returned csip service call metainfo section.");
      }

      metaError = resultMetaInfoObject.optString("error", "");

      if ((null != metaError) && (!metaError.isEmpty())) {
        JSONArray stackTrace = resultMetaInfoObject.optJSONArray("stacktrace");
        String stackString = "No trace available";
        if (null != stackTrace) {
          stackString = "";
          for (int i = 0; i < stackTrace.length(); i++) {
            stackString += stackTrace.getString(i) + System.lineSeparator();
          }
        }
        throwServiceCallException(metaError + "StackTrace: " + stackString);
      }

      callResultSection = this.results.optJSONArray("result");

    } catch (JSONException ex) {
      throwServiceCallException("Missing metainfo in return data. ", ex);
    }
  }

  protected void throwServiceCallException(String message) throws ServiceException {
    throw new ServiceException((errorPrefix.isEmpty() ? "" : (errorPrefix + ":  ")) + message);
  }

  protected void throwServiceCallException(String message, Exception ex) throws ServiceException {
    throw new ServiceException((errorPrefix.isEmpty() ? "" : (errorPrefix + ":  ")) + message + ex.getMessage(), ex);
  }

  protected final void execPost() throws ServiceException {
    createRequest();
    finished = false;

    try {
      results = super.doPOST(URI, request);
    } catch (Exception ex) {
      throwServiceCallException("Error making a connection to that location: " + URI + ".  " + ex.getMessage(), ex);
    }

    if (!asyncCall) {
      checkErrors();
      parseResults();
    } else {
      checkErrors();
      if (!finished) {
        parseAsyncResult();
      } else {
        parseResults();
      }
    }
  }
}