content: /**
   * ﻿Copyright (C) 2010 - 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.r.workspace;
  
  import java.io.ByteArrayInputStream;
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.UnsupportedEncodingException;
  import java.net.URL;
  import java.util.HashMap;
  
  import org.n52.wps.io.data.GenericFileData;
  import org.n52.wps.io.data.GenericFileDataConstants;
  import org.n52.wps.io.data.IData;
  import org.n52.wps.io.data.binding.complex.GenericFileDataBinding;
  import org.n52.wps.server.ExceptionReport;
  import org.n52.wps.server.r.RWPSConfigVariables;
  import org.n52.wps.server.r.RWPSSessionVariables;
  import org.n52.wps.server.r.R_Config;
  import org.n52.wps.server.r.syntax.RAnnotationException;
  import org.n52.wps.server.r.util.RExecutor;
  import org.n52.wps.server.r.util.RFileExtensionFilter;
  import org.n52.wps.server.r.util.RLogger;
  import org.rosuda.REngine.REXP;
  import org.rosuda.REngine.REXPMismatchException;
  import org.rosuda.REngine.Rserve.RConnection;
  import org.rosuda.REngine.Rserve.RserveException;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  public class RSessionManager {
  
      private static Logger log = LoggerFactory.getLogger(RSessionManager.class);
  
      private static final String NO_WARNINGS_MESSAGE = "The process proceeded without any warnings from R.";
  
      private static final String SESSION_INFO_OUTPUT_NAME = "sessionInfo";
  
      private static final String WARNING_OUTPUT_NAME = "warnings";
  
      private R_Config config;
  
      private RConnection connection;
  
      private boolean cleanOnStartup = true;
  
      public RSessionManager(RConnection rCon, R_Config config) {
          this.connection = rCon;
          this.config = config;
  
          log.debug("NEW {}", this);
      }
  
      private void cleanSession() {
          try {
              RLogger.log(connection, "CLEANING R SESSION!");
              this.connection.eval("rm(list = ls())");
          }
          catch (RserveException e) {
              log.error("Problem cleaning session", e);
          }
      }
  
      public void cleanUp() {
          log.debug("Cleaning up session...");
  
          RLogger.log(connection, "Session after process run:");
          RLogger.logSessionContent(connection);
  
          cleanSession();
      }
  
      public void configureSession(String processWKN, RExecutor executor) throws ExceptionReport,
              RserveException,
              FileNotFoundException,
              IOException,
              RAnnotationException {
          log.debug("Configuring R session...");
  
          if (cleanOnStartup) {
              cleanSession();
          }
  
          // configure memory limit
          StringBuilder cmd = new StringBuilder();
  
          String memoryLimit = config.getConfigVariable(RWPSConfigVariables.R_SESSION_MEMORY_LIMIT);
          cmd.append("memory.limit(");
          cmd.append(memoryLimit);
          cmd.append(")");
  
          try {
              REXP expr = this.connection.eval(cmd.toString());
              log.debug("Memory limit is '{}' (configuration value is '{}')", expr.asString(), memoryLimit);
              this.connection.eval("cat(\"Memory info > memory.size = \", toString(memory.size()), \" (max: \", toString(memory.size(TRUE)), \"); memory.limit = \", memory.limit(), \"\n\")");
          }
          catch (RserveException e) {
              log.error("Problem setting the memory limit", e);
          }
          catch (REXPMismatchException e) {
              log.error("Problem setting the memory limit", e);
          }
  
          loadWPSSessionVariables(processWKN);
          loadUtilityScripts(executor);
      }
  
      public String getConsoleOutput(String cmd) throws RserveException, REXPMismatchException {
          return this.connection.eval("paste(capture.output(print(" + cmd + ")), collapse='\\n')").asString();
      }
  
      public String getRVersion() throws RserveException, REXPMismatchException {
          return getConsoleOutput("R.version[\"version.string\"]");
      }
  
      public String getSessionInfo() throws RserveException, REXPMismatchException {
          return getConsoleOutput("sessionInfo()");
      }
  
      private void loadUtilityScripts(RExecutor executor) throws RserveException,
              IOException,
              FileNotFoundException,
              RAnnotationException,
              ExceptionReport {
          log.debug("Loading utility scripts.");
  
          File[] utils = new File(config.utilsDirFull).listFiles(new RFileExtensionFilter());
          for (File file : utils) {
              executor.executeScript(file, this.connection);
          }
  
          RLogger.log(connection, "workspace content after loading utility scripts:");
          RLogger.logSessionContent(connection);
      }
  
      private void loadWPSSessionVariables(String processWKN) throws ExceptionReport {
          log.debug("Loading session variables.");
  
          try {
              RLogger.log(connection, "Environment:");
              connection.eval("cat(capture.output(environment()), \"\n\")");
  
              String cmd = RWPSSessionVariables.WPS_SERVER + " <- TRUE";
              connection.eval(cmd);
              RLogger.logVariable(connection, RWPSSessionVariables.WPS_SERVER);
  
              cmd = RWPSSessionVariables.WPS_SERVER_NAME + " <- \"52N-WPS\"";
              connection.eval(cmd);
              RLogger.logVariable(connection, RWPSSessionVariables.WPS_SERVER_NAME);
  
              String resourceUrl = config.getResourceDirURL();
              connection.assign(RWPSSessionVariables.RESOURCE_URL_NAME, resourceUrl);
              log.debug("Assigned resource directory to variable '{}': {}",
                        RWPSSessionVariables.RESOURCE_URL_NAME,
                        resourceUrl);
              RLogger.logVariable(connection, RWPSSessionVariables.RESOURCE_URL_NAME);
  
              URL processDescription = config.getProcessDescriptionURL(processWKN);
  
              connection.assign(RWPSSessionVariables.PROCESS_DESCRIPTION, processDescription.toString());
              RLogger.logVariable(connection, RWPSSessionVariables.PROCESS_DESCRIPTION);
  
              log.debug("Assigned process description to variable '{}': {}",
                        RWPSSessionVariables.PROCESS_DESCRIPTION,
                        processDescription);
  
              // create session variable for warning storage
              cmd = RWPSSessionVariables.WARNING_OUTPUT_STORAGE + " = c()";
              connection.eval(cmd);
              RLogger.logVariable(connection, RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
  
              RLogger.log(connection, "workspace content after loading session variables:");
              RLogger.logSessionContent(connection);
          }
          catch (RserveException e) {
              log.error("Error loading WPS session variables for process {}", processWKN);
              throw new ExceptionReport("Could not load session variables for " + processWKN,
                                        ExceptionReport.REMOTE_COMPUTATION_ERROR,
                                        e);
          }
      }
  
      /**
       * Retrieves warnings that occured during the last execution of a script
       * 
       * Note that the warnings()-method is not reliable for Rserve because it does not return warnings in most
       * cases. Therefore a specific warnings function is used to retrieve the warnings.
       */
      private String getWarnings() throws RserveException, REXPMismatchException {
          REXP result = connection.eval(RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
  
          StringBuilder warnings = new StringBuilder();
          if ( !result.isNull()) {
              String[] warningsArray = result.asStrings();
              for (int i = 0; i < warningsArray.length; i++) {
                  String currentWarning = warningsArray[i];
  
                  warnings.append("warning ");
                  warnings.append( (i));
                  warnings.append(": '");
                  warnings.append(currentWarning);
                  warnings.append("'\n");
              }
          }
  
          if (warnings.length() < 1)
              return NO_WARNINGS_MESSAGE;
          return warnings.toString();
      }
  
      public HashMap<String, IData> saveInfos(HashMap<String, IData> result) {
          try {
              String sessionInfo = getSessionInfo();
              InputStream sessionInfoStream = new ByteArrayInputStream(sessionInfo.getBytes("UTF-8"));
              result.put(SESSION_INFO_OUTPUT_NAME,
                         new GenericFileDataBinding(new GenericFileData(sessionInfoStream,
                                                                        GenericFileDataConstants.MIME_TYPE_PLAIN_TEXT)));
              sessionInfoStream.close();
  
              String warnings = getWarnings();
              InputStream warningsStream = new ByteArrayInputStream(warnings.getBytes("UTF-8"));
              result.put(WARNING_OUTPUT_NAME,
                         new GenericFileDataBinding(new GenericFileData(warningsStream,
                                                                        GenericFileDataConstants.MIME_TYPE_PLAIN_TEXT)));
              warningsStream.close();
          }
          catch (UnsupportedEncodingException e) {
              log.error("Could not save session info and warnings.", e);
          }
          catch (IOException e) {
              log.error("Could not save session info and warnings.", e);
          }
          catch (REXPMismatchException e) {
              log.error("Could not save session info and warnings.", e);
          }
          catch (RserveException e) {
              log.error("Could not save session info and warnings.", e);
          }
  
          return result;
      }
  
  }
  