UIService.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.net.URI;
import java.text.DateFormat;
import java.util.*;
import java.util.logging.*;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import org.apache.commons.io.FileUtils;
import org.codehaus.jettison.json.JSONException;

/**
 * ControlService
 *
 * @author od
 */
@Path("ui")
public class UIService {

  // The ip Info website.
  static final String ipInfo = "http://ip-api.com/#";

  static final String ARCHIVE_HEADER = "<tr bgcolor=\"#D5DADE\">"
      + "<th>suid</th>"
      + "<th>status</th>"
      + "<th>req IP</th>"
      + "<th>archived at</th>"
      + "<th>expiration</th>"
      + "<th>service</th>"
      + "<th>files</th>"
      + "<th> </th>"
      + "</tr>\n";

  static final String ARCHIVE_ROW = "<tr bgcolor=\"%6$s\">"
      + "<td>%1$s</td>"
      + "<td>%2$s</td>"
      + "<td><a href=\"" + ipInfo + "%4$s\">%3$s</a></td>"
      + "<td>%4$s</td>"
      + "<td>%8$s</td>"
      + "<td>%5$s</td>"
      + "<td><a href=\"/%7$s/a/download/%1$s\">download</a></td>"
      + "<td><a href=\"/%7$s/a/remove/%1$s\">del</a> </td>"
      + "</tr>\n";

  static final String HEADER = "<tr bgcolor=\"#D5DADE\">"
      + "<th colspan=2>suid/json</th>"
      + "<th>status</th>"
      + "<th>node IP</th>"
      + "<th>req IP</th>"
      + "<th>start</th>"
      + "<th>expiration</th>"
      + "<th>runtime</th>"
      + "<th>service</th>"
      + "<th>attach</th>"
      + "<th> </th>"
      + "</tr>\n";
  //
  static final String DEF_BACKGROUND = "#F0F0F0";
  static final String ZOMBIE_BACKGROUND = "#E19BFF";    //purple
  static final String FAILED_BACKGROUND = "#FFCFD8";    // red
  static final String FINISHED_BACKGROUND = "#D7FFC2";  // green

  static final Logger LOG = Config.LOG;


  static StringBuilder appendLine(StringBuilder b, String col, String... td) {
    b.append("<tr bgcolor=\"").append(col).append("\">");
    for (String t : td) {
      b.append("<td>").append(t).append("</td>");
    }
    b.append("</tr>\n");
    return b;
  }


  static String getIPLink(String ip) {
    if ("127.0.0.1".equals(ip)) 
      return ip;
    
    StringBuilder b = new StringBuilder();
    b.append("<a href=\"" + ipInfo).append(ip).append("\">").append(ip).append("</a></td>");
    return b.toString();
  }


  static String getLinks(String[] row, String context) {
    boolean service_done = false;
    String status = row[1];
    String suid = row[0];
    String hasReport = row[9];

    if (status.equals(ModelDataService.FAILED) || status.equals(ModelDataService.FINISHED)) 
      service_done = true;

    StringBuilder b = new StringBuilder();
    b.append("<a href=\"/").append(context).append("/q/req/").append(suid).append("\">req</a>");
    if (service_done) {
      b.append("&nbsp;&nbsp;<a href=\"/").append(context).append("/q/res/").append(suid).append("\">res</a>");
      if (Boolean.parseBoolean(hasReport)) 
        b.append("&nbsp;&nbsp;<a href=\"/").append(context).append("/r/").append(suid).append("\">rep</a>");
    }
    return b.toString();
  }


  String getLogLink(String context, String id) {
    return "<a href=\"/" + context + "/q/log/" + id + "\">log</a>";
  }

/////////////////////////////////

  /**
   * Session UI
   *
   * @param u
   * @param skip
   * @param limit
   * @param sort
   * @param order
   * @return
   * @throws Exception
   */
  @GET
//    @Path("")
  @Produces(MediaType.TEXT_HTML)
  public String getSessions(@Context UriInfo u, @Context HttpServletRequest req,
      @DefaultValue("0")
      @QueryParam("skip") int skip,
      @DefaultValue("25")
      @QueryParam("limit") int limit,
      @DefaultValue("tst")
      @QueryParam("sort") String sort,
      @DefaultValue("d")
      @QueryParam("order") String order)
      throws Exception {

    Utils.checkRemoteAccessACL(req);

    URI uri = u.getBaseUri();
    String path = uri.getPath();
    String context = uri.getPath();
    context = context.substring(1, context.indexOf('/', 2));

    SessionStore s = Config.getSessionStore();
    long start = System.currentTimeMillis();
    Set<String> keys = s.keys(skip, limit, sort, order.equalsIgnoreCase("a"));

    Date now = new Date();
    List<String[]> l = new ArrayList<>();
    for (String id : keys) {
      ModelSession v = s.getSession(id);
      if (v == null) 
        continue;  // this should not happen
      
      Date startsim = Dates.parse(v.getTstamp());
      String cputime = v.getCputime();
      String duration = cputime.isEmpty() ? Dates.duration(startsim, now) : Dates.duration(Long.parseLong(cputime));
      l.add(new String[]{
        id.substring(id.indexOf(':') + 1),
        v.getStatus(),
        v.getNodeIP(),
        v.getReqIP(),
        v.getTstamp(),
        v.getExpDate(),
        duration,
        v.getService(),
        Arrays.toString(v.getAttachments()),
        Boolean.toString(v.hasReport())
      });
    }

    int this_start = skip + 1;
    int this_end = keys.size() + skip;
    int prev_start = this_start - limit - 1;
    int next_start = this_end;

    long count = s.getCount();

    String prev_link = prev_start < 0 ? "" : "href=\"" + path + "ui?skip=" + prev_start + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"";
    String next_link = count == this_end ? "" : "href=\"" + path + "ui?skip=" + next_start + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"";

    String prev = "<a " + prev_link + "><</a>";
    String next = "<a " + next_link + ">></a>";
    String begin = "<a " + "href=\"" + path + "ui?skip=" + 0 + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"" + "><<</a>";
    String end = "<a " + "href=\"" + path + "ui?skip=" + (count - count % limit) + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"" + ">>></a>";

    StringBuilder b = new StringBuilder();
    b.append("<html><head><title>Sessions - " + context + "</title></head><body>");
    b.append("<table width=\"100%\" border=\"0\" cellpadding=\"1\"><tr>");
    b.append("<td  width=\"33%\">");
    b.append("<a href=\"" + path + "ui/archive\">Archive</a> | ");
    b.append("<a href=\"" + path + "\">Services</a> ");
    b.append("<a href=\"" + path + "ui/conf\">Config</a> | ");
    b.append("<a href=\"" + path + "q/check\">Check</a> ");
    b.append("<a href=\"" + path + "c/clean\">Clean</a>");
//        b.append("<a href=\"" + path + "a/clean\">CleanArchive</a> ");
    b.append("</td>");
    b.append("<td width=\"33%\">");
    b.append("<p align=\"center\">" + begin + "&nbsp;&nbsp;" + prev + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
        + this_start + " - " + this_end + " (of " + count + ") &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + next + "&nbsp;&nbsp;" + end + "</p>");
    b.append("</td width=\"33%\">");
    b.append("<td><p align=\"right\"> <b>Sessions</b> at ");
    b.append(Dates.nowISO(Config.getString("csip.timezone", "UTC")));
    b.append("</p></td>");
    b.append("</tr></table>");

    b.append("<table width=\"100%\" border=\"0\" cellpadding=\"5\">");
    b.append("<tbody align=\"left\" style=\"font-family:monospaced;\">");
    b.append(HEADER);
    DateFormat df = Dates.newISOFormat();
    String no = df.format(new Date());

    for (String[] c : l) {
      String status = c[1];
      String color = DEF_BACKGROUND;
      String exp = c[5];
      if (status.equals(ModelDataService.FAILED)) {
        color = FAILED_BACKGROUND;
      } else if (status.equals(ModelDataService.FINISHED)) {
        color = FINISHED_BACKGROUND;
      }
      if (!exp.isEmpty() && exp.compareTo(no) < 0) {  // check is zombie that is past expiration
        color = ZOMBIE_BACKGROUND;
      }
      String links = getLinks(c, context);
      String iplink = getIPLink(c[3]);
      String logLink = getLogLink(context, c[0]);
      String canLink = "<a href=\"/" + context + "/c/cancel/" + c[0] + "\">stop</a>";
      appendLine(b, color, c[0], links, c[1], c[2], iplink, c[4], c[5], c[6], c[7], c[8], logLink + " " + canLink);
    }
    b.append("</tbody></table>");
    long end_ = System.currentTimeMillis();
    b.append("<em>" + (end_ - start) + " ms</em>");
    b.append("</body><html>");
    return b.toString();
  }


  /**
   * Archive UI.
   *
   * @param u
   * @param skip
   * @param limit
   * @param sort
   * @param order
   * @return
   * @throws Exception
   */
  @GET
  @Path("archive")
  @Produces(MediaType.TEXT_HTML)
  public String getArchives(@Context UriInfo u, @Context HttpServletRequest req,
      @DefaultValue("0") @QueryParam("skip") int skip,
      @DefaultValue("25") @QueryParam("limit") int limit,
      @DefaultValue("ctime") @QueryParam("sort") String sort,
      @DefaultValue("d") @QueryParam("order") String order) // descending order
      throws Exception {

    Utils.checkRemoteAccessACL(req);

    if (!Config.isArchiveEnabled()) 
      return "<html><body>Archive not enabled.</body></html>";

    ArchiveStore s = Config.getArchiveStore();
    if (!s.isAvailable()) 
      return "<html><body>Archive not accessible.</body></html>";

    URI uri = u.getBaseUri();
    String path = uri.getPath();
    String context = uri.getPath();
    context = context.substring(1, context.indexOf('/', 2));

    Set<String> keys = s.keys(skip, limit, sort, order.equalsIgnoreCase("a"));

    long start = System.currentTimeMillis();
    List<String[]> l = new ArrayList<>();
    for (String id : keys) {
      ModelArchive v = s.getArchive(id);
      if (v == null) 
        continue;  // this should not happen
      
      l.add(new String[]{
        id,
        v.getStatus(),
        v.getReqIP(),
        v.getCtime(),
        v.getService(),
        v.getEtime()
      });
    }

    int this_start = skip + 1;
    int this_end = keys.size() + skip;
    int prev_start = this_start - limit - 1;
    int next_start = this_end;

    String prev_link = prev_start < 0 ? "" : "href=\"" + path + "ui/archive?skip=" + prev_start + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"";
    String next_link = s.getCount() == this_end ? "" : "href=\"" + path + "ui/archive?skip=" + next_start + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"";

    String prev = "<a " + prev_link + "><</a>";
    String next = "<a " + next_link + ">></a>";
    String begin = "<a " + "href=\"" + path + "ui/archive?skip=" + 0 + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"" + "><<</a>";
    String end = "<a " + "href=\"" + path + "ui/archive?skip=" + (s.getCount() - s.getCount() % limit) + "&limit=" + limit + "&sort=" + sort + "&order=" + order + "\"" + ">>></a>";

    // create the page.
    StringBuilder b = new StringBuilder();
    b.append("<html><head><title> Archives - " + context + "</title></head><body>");

    b.append("<table width=\"100%\" border=\"0\" cellpadding=\"1\"><tr>");
    b.append("<td  width=\"33%\">");
    b.append("<a href=\"" + path + "ui\">Sessions</a>  ");
//    b.append("<a href=\"" + path + "a/clean\">Clean</a> ");
    b.append("</td>");
    b.append("<td width=\"33%\">");
    b.append("<p align=\"center\">" + begin + "&nbsp;&nbsp;" + prev + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
        + this_start + " - " + this_end + " (of " + s.getCount() + ") &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + next + "&nbsp;&nbsp;" + end + "</p>");
    b.append("</td width=\"33%\">");
    b.append("<td><p align=\"right\"> <b>Archives</b> at ");
    b.append(Dates.nowISO(Config.getString("csip.timezone", "UTC")));
    b.append("</p></td>");
    b.append("</tr></table>");

    b.append("<table width=\"100%\" border=\"0\" cellpadding=\"5\">");
    b.append("<tbody align=\"left\" style=\"font-family:monospaced;\">");
    b.append(ARCHIVE_HEADER);
    for (String[] c : l) {
      String color = c[1].equals(ModelDataService.FAILED) ? FAILED_BACKGROUND : FINISHED_BACKGROUND;
      b.append(String.format(ARCHIVE_ROW, c[0], c[1], c[2], c[3], c[4], color, context, c[5]));
    }
    b.append("</tbody></table>");
    long endtime = System.currentTimeMillis();
    b.append("<em>" + (endtime - start) + " ms</em>");
    b.append("</body><html>");
    return b.toString();
  }


  /**
   * Get the service configuration
   * @return
   * @throws JSONException
   */
  @GET
  @Path("conf")
  @Produces(MediaType.TEXT_HTML)
  public String getHtml(@Context UriInfo u, @Context HttpServletRequest req) throws JSONException {
    Utils.checkRemoteAccessACL(req);

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

    String resources = " cores:" + Integer.toString(Runtime.getRuntime().availableProcessors())
        + " mem_max:" + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().maxMemory())
        + " mem_total:" + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().totalMemory())
        + " mem_free:" + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().freeMemory());

    return "<html><body><h1>CSIP Configuration </h1>" + Services.LOCAL_IP_ADDR + "</br> " + resources + "<p>" + o.toString() + "</p></body></html>";
  }
}