ContextConfig.java [src/csip] Revision: c802c3713e63fadc2ad83d9bc14e04b18eaf38c0  Date: Wed Apr 26 15:57:16 MDT 2017
/*
 * $Id$
 *
 * This file is part of the Cloud Services Integration Platform (CSIP),
 * a Model-as-a-Service framework, API and application suite.
 *
 * 2012-2017, 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.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
 */
public class ContextConfig {

    static final Logger LOG = Logger.getLogger(ContextConfig.class.getName());

    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 HashMap<>();
    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();
            String value = ctx.getInitParameter(key);
            conf.put(key, value);
        }
    }


    /**
     * 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
     */
    public static void filterServices(ServletContext c, Set<Class<?>> orig) {
        try {
            ContextConfig cc = new ContextConfig();
            cc.load(c, ".yaml");
            cc.loadServices(c);

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

            // create a lookup table
            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) {
                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 (cl.getCanonicalName().startsWith("m.")
                                || cl.getCanonicalName().startsWith("d.")) {
                            filtered_resources.add(cl);
                        }
                    } else {
                        LOG.warning("service not found in context: '" + s.trim() + "', ignoring.");
                    }
                }
            }
            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("Context configuration: '" + 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.equals(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));
        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;
    }

}