ContextConfig.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 java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.ws.rs.Path;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.scanner.ScannerException;

/**
 *
 * @author od
 */
class ContextConfig {

  static final Logger LOG = Config.LOG;
  //
  static final String API_VERSION = "csip/2.1";
  //
  static final String KEY_API = "Api";
  static final String KEY_CONFIG = "Config";
  static final String KEY_SERVICES = "Services";
  //
  private Map<String, String> conf = new TreeMap<>();
  private List<String> services = new ArrayList<>();


  /**
   * Configuration from a file.
   *
   * @param ctx
   */
  void load(File file) {
    if (file == null || !file.exists() || !file.canRead()) 
      return;
    
    try {
      init(new FileInputStream(file), file.toString());
    } catch (FileNotFoundException E) {
      LOG.log(Level.WARNING, "Error", E);
    }
  }


  /**
   * Configuration from servlet init parameter.
   *
   * @param ctx
   */
  void load(ServletContext ctx) {
    if (ctx == null) {
      LOG.log(Level.SEVERE, "No service context");
      return;
    }
    Enumeration<String> params = ctx.getInitParameterNames();
    while (params.hasMoreElements()) {
      String key = params.nextElement();
      conf.put(key, ctx.getInitParameter(key));
    }
  }


  /**
   * Configuration from bundled file within the context.
   *
   * @param ctx
   * @param file
   */
  void load(ServletContext ctx, String file) {
    if (ctx == null) {
      LOG.log(Level.SEVERE, "No service context for " + file);
      return;
    }
    if (file.startsWith("/")) {
      // this is a file within the context. /META-INF/config.json
      InputStream is = ctx.getResourceAsStream(file);
      if (is == null) {
        return;
      }
      init(is, file);
    } else {
      // this is looking for a file csip-abc##1.2.3-1.2.3.json
      File f = getContextFile(ctx, file);
      if (f != null) {
        load(f);
      }
    }
  }


  /**
   * read the list from a text file "??.services" in webapps.
   *
   * @param c
   */
  void loadServices(ServletContext c) {
    try {
      File serviceList = getContextFile(c, ".services");
      // List to filter on.
      if (serviceList == null) {
        return;
      }
      List<String> srv = FileUtils.readLines(serviceList, "utf-8");
      for (String s : srv) {
        if (!s.isEmpty() && !s.trim().startsWith("#")) {
          services.add(s.trim());
        }
      }
    } catch (IOException ex) {
      LOG.log(Level.SEVERE, null, ex);
    }
  }


  /**
   * Filters the services as specified in config files.
   * @param c the servlet context
   * @param orig the original set of service classes
   */
  static void filterServices(ServletContext c, Set<Class<?>> orig) {
    try {
      // just to make sure it's in there, even if 
      // orig is not being filtered. Since it is a Set,
      // the class MultiPartFeature will not be in orig
      // twice.
      orig.add(MultiPartFeature.class);

      // Add the internal services if not already in orig.
      orig.add(QueueingModelDataService.class);
      orig.add(DynamicPyModelDataService.class);

      ContextConfig cc = new ContextConfig();
      // load services from webapp's  <context>.yaml
      cc.load(c, ".yaml");
      // load services from webapp's  <context>.services
      cc.loadServices(c);

      // It has to be anabled!
      if (!Config.getBoolean(Config.CSIP_PUBSUB_ENABLED, false)) {
        orig.remove(QueueingModelDataService.class);
      }

      if (!Config.getBoolean(Config.CSIP_DYNPY_ENABLED, false)) {
        orig.remove(DynamicPyModelDataService.class);
      }

      if (cc.getServices().isEmpty()) {
        // nothing to do.
        return;
      }

      // create a lookup table 'service path' -> 'service class'
      Map<String, Class<?>> smap = new HashMap<>();
      orig.forEach(cl -> {
        Path p = cl.getAnnotation(Path.class);
        if (p != null) {
          String s = p.value();
          if (s != null) {
            smap.put(s, cl);
          }
        }
      });

      Set<Class<?>> filtered_resources = new HashSet<>();
      // add internal classes
      filtered_resources.add(MultiPartFeature.class);
      for (Class<?> cl : orig) {
        // all internal core services
        if (cl.getCanonicalName().startsWith("csip.")) {
          filtered_resources.add(cl);
        }
      }

      for (String s : cc.getServices()) {
        // allow for comment lines and empty lines (properties file)
        if (s != null && !s.isEmpty()) {
          String service = s.trim();
          // be more forgiving
          if (service.startsWith("/")) {
            service = service.substring(1);
          }
          Class<?> cl = smap.get(service);
          if (cl != null) {
            // you can only request model and data services to be added.
            // using package conventions
            if (Utils.isCsipService(cl)) {
              filtered_resources.add(cl);
            }
          } else {
            LOG.warning("service not found in context: '" + s.trim() + "', ignoring.");
          }
        }
      }
      // swap the set
      orig.clear();
      orig.addAll(filtered_resources);
    } catch (Exception ex) {
      LOG.log(Level.WARNING, "Error applying the service filter ", ex);
      LOG.info("Using all services.");
    }
  }


  private void init(InputStream is, String file) {
    try {
      LOG.info("        Loading settings from: " + file);
      String ftype = file.substring(file.lastIndexOf('.'));
      switch (ftype) {
        case ".yaml":
          fromYaml(is);
          break;
        case ".properties":
          fromProperties(is);
          break;
        case ".json":
          fromJson(is);
          break;
        default:
          LOG.warning("Illegal configuration file: " + file);
      }
    } catch (ScannerException E) {
      LOG.log(Level.WARNING, "Ignoring config, Error:\n" + E.getMessage());
    } catch (Exception E) {
      LOG.log(Level.WARNING, "Ignoring config, Error:\n" + E);
    } finally {
      try {
        is.close();
      } catch (Exception ex) {
        // can be ignored.
      }
    }
  }


  /**
   * This is config properties only!
   *
   * @param is
   * @throws IOException
   */
  private void fromProperties(InputStream is) throws IOException {
    Properties p = new Properties();
    p.load(is);
    if (p.isEmpty()) 
      return;
    
    p.stringPropertyNames().forEach((key) -> {
      conf.put(key, p.getProperty(key));
    });
  }


  @SuppressWarnings("unchecked")
  private void fromYaml(InputStream is) {
    Map<String, Object> yaml = (Map) new Yaml().load(is);
    if (!yaml.containsKey(KEY_API)) 
      throw new RuntimeException("Missing Api version: 'Api: " + API_VERSION + "', ignoring.");
    
    String apiversion = (String) yaml.get(KEY_API);
    if (!apiversion.equalsIgnoreCase(API_VERSION)) 
      throw new RuntimeException("Invalid Api version: " + apiversion + " ignoring.");
    
    if (!yaml.containsKey(KEY_CONFIG)) 
      throw new RuntimeException("Missing: 'Config: ...', ignoring.");
    

    Map<Object, Object> j = (Map) yaml.get(KEY_CONFIG);
    for (Map.Entry<Object, Object> entry : j.entrySet()) {
      conf.put(entry.getKey().toString(), entry.getValue().toString());
    }

    if (yaml.containsKey(KEY_SERVICES)) {
      for (Object entry : (List) yaml.get(KEY_SERVICES)) {
        services.add(entry.toString());
      }
    }
  }


  private void fromJson(InputStream is) throws IOException, JSONException {
    JSONObject o = new JSONObject(IOUtils.toString(is, "UTF-8"));
    if (o.has(KEY_API)) {
      String apiversion = o.getString(KEY_API);
      if (!apiversion.equals(API_VERSION)) 
        throw new RuntimeException("Invalid Api version: " + apiversion);
    }
    JSONObject c = o;
    if (o.has(KEY_CONFIG)) 
      c = o.getJSONObject(KEY_CONFIG);
    
    Iterator<?> i = c.keys();
    while (i.hasNext()) {
      String k = i.next().toString();
      String v = c.getString(k);
      conf.put(k, v);
    }
    if (o.has(KEY_SERVICES)) {
      JSONArray arr = o.getJSONArray(KEY_SERVICES);
      for (int j = 0; j < arr.length(); j++) {
        services.add(arr.getString(j));
      }
    }
  }


  private File getContextFile(ServletContext c, String ext) {
    if (c == null) 
      return null;
    
    String s = c.getRealPath("/WEB-INF");
    if (s != null) {
      File f = new File(s).getParentFile();
      if (f != null && f.exists()) {
        File file = new File(f.getParentFile(), f.getName() + ext);
        if (file.exists() && file.canRead()) 
          return file;
      }
    }
    return null;
  }


  Map<String, String> getConfig() {
    return conf;
  }


  List<String> getServices() {
    return services;
  }

}