ControlService.java [src/csip] 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-2022, Olaf David and others, 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 csip;

import csip.utils.*;
import java.io.*;
import java.text.DateFormat;
import java.util.*;
import java.util.logging.*;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.client.*;
import javax.ws.rs.core.*;
import org.apache.commons.io.*;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

/**
 * ControlService. Use services in this class to control the backend.
 *
 * @author od
 */
@Path("c")
public class ControlService {

  static final Logger LOG = Config.LOG;


  /**
   * Clean up the whole session store.
   *
   * @param uriInfo uri info
   * @param req servlet request
   * @return string of total, removed, null, skipped sessions
   * @throws JSONException when analyzing/building JSONObjects
   * @throws Exception when analyzing the servlet request
   */
  @GET
  @Path("clean")
  @Produces(MediaType.APPLICATION_JSON)
  public String getClean(@Context UriInfo uriInfo, @Context HttpServletRequest req) throws JSONException, Exception {
    Utils.checkRemoteAccessACL(req);

    SessionStore s = Config.getSessionStore();
    Set<String> keys = s.keys(0, Integer.MAX_VALUE, null, true);
    int isnull = 0;
    int removed = 0;
    int skipped = 0;
    for (String id : keys) {
      String result = remove(uriInfo, req, id);
      JSONObject r = new JSONObject(result);
      if (r.has("isnull")) {
        isnull++;
      } else if (r.has("removed")) {
        removed++;
      } else if (r.has("skipped")) {
        skipped++;
      }
    }
    JSONObject o = new JSONObject();
    o.put("sessions.total", keys.size());
    o.put("sessions.removed", removed);
    o.put("sessions.isnull", isnull);
    o.put("sessions.skipped", skipped);
    return o.toString(4);
  }


  /**
   * Remove the session if expired. cleanup also the workspace and results.
   *
   * @param uriInfo uri info
   * @param req servlet request
   * @param suid session uid
   * @return string of cleanup session
   * @throws Exception when getting ModelSession and building JSONObject
   */
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Path("clean/{suid}")
  public String remove(@Context UriInfo uriInfo, @Context HttpServletRequest req, @PathParam("suid") String suid) throws Exception {
    Utils.checkRemoteAccessACL(req);

    ModelSession session = Config.getSessionStore().getSession(suid);
    JSONObject o = new JSONObject();
    if (session == null) {
      o.put("isnull", 1);
      return o.toString();
    }
    if (session.getNodeIP().equals(Services.LOCAL_IP_ADDR)) {
      DateFormat df = Dates.newISOFormat();
      String now = df.format(new Date());
      String exp = session.getExpDate();
      if (exp.compareTo(now) < 0) {  // check is zombie that is passt expiration
        File res = Services.getResultsDir(suid);
        if (res.exists()) {
          FileUtils.deleteQuietly(res);
        }
        File work = Services.getWorkDir(suid);
        if (work.exists()) {
          FileUtils.deleteQuietly(work);
        }
        Config.getSessionStore().removeSession(suid);
        o.put("removed", 1);
        return o.toString();
      }
      o.put("skipped", 1);
      return o.toString();
    } else {
      String redirect = Utils.replaceHostinURI(uriInfo.getBaseUri(), session.getNodeIP());
      LOG.info("Redirect query to: " + redirect + "c/clean/" + suid);
      javax.ws.rs.client.Client client = ClientBuilder.newClient();
      WebTarget service = client.target(redirect + "c/clean/" + suid);
      Response response = service.request(MediaType.APPLICATION_JSON).get();
      return response.readEntity(String.class);
    }
  }


  /**
   * Get the Configuration as JSON.
   *
   * @param uriInfo uri info
   * @param req servlet request
   * @return the configuration as a String
   * @throws JSONException when building JSONObject configuration
   */
  @GET
  @Path("conf")
  @Produces(MediaType.APPLICATION_JSON)
  public String getJSON(@Context UriInfo uriInfo, @Context HttpServletRequest req) throws JSONException {
    Utils.checkRemoteAccessACL(req);

    Properties p = Config.getMergedProperties();
    Map<Object, Object> m = new TreeMap<>();
    for (Object key : p.keySet()) {
      String key_ = key.toString();
      String val = p.get(key).toString();
      val = val.replaceAll("password=.+[;]?", "password=****;");
      if (key_.toLowerCase().contains("password")) {
        val = "****";
      }
      m.put(key_, val);
    }

    JSONObject theconfig = new JSONObject(m);
    return theconfig.toString(2);
  }


  // update from properties, this is for silent property update.
  static void updateConfig(Properties p1) {
    if (p1.isEmpty()) {
      return;
    }
    Properties p = Config.getProperties();
    synchronized (p) {
      for (String key : p1.stringPropertyNames()) {
        String val = p1.getProperty(key);
        LOG.info("Set Config: " + key + " = " + val);
        p.setProperty(key, val);
      }
      Config.update();
    }
  }


  // update from YAML, this is for silent property update.
  static void updateConfig(Map<String, String> p1) {
    if (p1.isEmpty()) {
      return;
    }
    Properties p = Config.getProperties();
    synchronized (p) {
      for (String key : p1.keySet()) {
        String val = p1.get(key);
        if (val != null) {
          LOG.info("        " + key + " = " + val);
          p.setProperty(key, val);
        }
      }
      Config.update();
    }
  }


  static String updateConfig(String inputObj) throws JSONException {
    Properties p = Config.getProperties();
    synchronized (p) {
      try {
        JSONObject o = new JSONObject(inputObj);
        Iterator<?> i = o.keys();
        while (i.hasNext()) {
          String key = i.next().toString();
          if (key.trim().equals("#") || key.trim().equals("//") || key.toLowerCase().startsWith("note")) {
            continue;
          }
          LOG.info("  Set Config: " + key + " = " + o.getString(key));
          p.setProperty(key, o.getString(key));
        }
      } catch (JSONException | NumberFormatException ex) {
        LOG.log(Level.WARNING, null, ex);
        return "Error: " + ex.getMessage();
      }
      Config.update();
    }
    LOG.log(Level.INFO, "config complete");
    JSONObject theconfig = new JSONObject(new TreeMap<>(p));
    return theconfig.toString(2);
  }


  /**
   * Change the configuration.
   *
   * @param uriInfo uri info
   * @param req servlet request
   * @param inputObj request as string
   * @return updated configuration as string
   * @throws JSONException when updating the configuration
   */
  @POST
  @Path("conf")
  @Consumes(MediaType.APPLICATION_JSON)
  public String putJSON(@Context UriInfo uriInfo, @Context HttpServletRequest req, String inputObj) throws JSONException {
    LOG.log(Level.INFO, "HTTP/POST {0}", uriInfo.getRequestUri().toString() + " " + inputObj);
    Utils.checkRemoteAccessACL(req);
    return updateConfig(inputObj);
  }


  /**
   * Cancel the request.
   *
   * @param uriInfo uri info
   * @param suid session uid
   * @return error/canceled/unknown suid
   * @throws Exception when getting ModelSession, modifying/building uri
   */
  @GET
  @Path("cancel/{suid}")
  @Produces(MediaType.APPLICATION_JSON)
  public String cancel(@Context UriInfo uriInfo, @PathParam("suid") String suid) throws Exception {
    LOG.log(Level.INFO, "HTTP/GET {0}", uriInfo.getRequestUri().toString());
    ModelSession session = Config.getSessionStore().getSession(suid);
    if (session == null) {
      return JSONUtils.error("suid unknown: " + suid).toString();
    }

    if (session.getNodeIP().equals(Services.LOCAL_IP_ADDR)) {
      // check the local box first.
      for (ModelDataService.Task modelTask : Config.getModelTasks()) {
        if (modelTask.getService().getSUID().equals(suid)) {
          modelTask.cancel();
          LOG.log(Level.INFO, "canceled " + suid);
          return JSONUtils.ok(suid + " canceled").toString();
        }
      }
      LOG.log(Level.WARNING, " not found to cancel: " + suid);
      return JSONUtils.error("suid unknown: " + suid).toString();
    } else {
      // redrection here
      // where is that session?
      String redirect = Utils.replaceHostinURI(uriInfo.getBaseUri(), session.getNodeIP());
      javax.ws.rs.client.Client client = ClientBuilder.newClient();
      WebTarget service = client.target(UriBuilder.fromUri(redirect + "c/cancel/" + suid).build());
      return service.request().get().readEntity(String.class);
    }
  }


  @GET
  @Path("cleanresults")
  @Produces(MediaType.APPLICATION_JSON)
  public String dropResults(@Context UriInfo uriInfo) {
    LOG.log(Level.INFO, "HTTP/GET {0}", uriInfo.getRequestUri().toString());
    Config.getResultStore().purge();
    return JSONUtils.ok("purged").toString();
  }

}