Binaries.java [src/csip/utils] Revision: 5361f9568617edb3e9d14a5770e76c883ce9f96c  Date: Mon Apr 04 10:46:52 MDT 2016
/*
 * $Id$
 * 
 * This file is part of the Cloud Services Integration Platform (CSIP),
 * 2010-2013, Olaf David and others, Colorado State University.
 *
 * CSIP is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 2.1.
 *
 * CSIP 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with OMS.  If not, see <http://www.gnu.org/licenses/lgpl.txt>.
 */
package csip.utils;

import csip.Config;
import csip.Executable;
import csip.ServiceException;
import csip.SessionLogger;
import csip.annotations.Resource;
import csip.annotations.ResourceType;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLConnection;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.apache.tomcat.jdbc.pool.DataSourceProxy;
import org.apache.tomcat.jdbc.pool.PoolProperties;

/**
 *
 * @author od
 */
public class Binaries {

    public static final String STDOUT = "-stdout.txt";
    public static final String STDERR = "-stderr.txt";

    private static class JDBCPool {

        DataSource datasource;
        String url;
        String resolved_url = "";
        Map<String, String> env;


        JDBCPool(String url, Map<String, String> env) {
            this.url = url;
            this.env = env;
        }


        Connection getConnection(SessionLogger logger) throws ServiceException {
            try {
                // probe for configuration change.
                String res_url = resolve(url);
                if (datasource == null || !res_url.equals(resolved_url)) {
                    //Close previous datasource if it exists
                    if (datasource != null) {
                        shutdown();
                    }

                    //Remember this connection string in case it has changed.
                    resolved_url = res_url;
                    PoolProperties p = new PoolProperties();
                    defaultProperties(p);
                    p.setUrl(resolved_url);
                    logger.info(" JDBC pool property:  url:" + resolved_url);
                    // load the driver
                    if (resolved_url.toLowerCase().contains(":postgres:") && !env.containsKey("driverClassName")) {
                        env.put("driverClassName", "org.postgresql.Driver");
                    }
                    if (resolved_url.toLowerCase().contains(":sqlserver:") && !env.containsKey("driverClassName")) {
                        env.put("driverClassName", "com.microsoft.sqlserver.jdbc.SQLServerDriver");
                    }

                    // overwrite the defaults.
                    for (String key : env.keySet()) {
                        BeanUtils.setProperty(p, key, env.get(key));
                        logger.info("env JDBC pool property: " + key + ": " + env.get(key));
                    }

                    logger.info("JDBC Pool properties:" + p.toString());
                    datasource = new org.apache.tomcat.jdbc.pool.DataSource(p);
//                    datasource.setLoginTimeout();
                    logger.info("Created JDBC datasource: " + datasource.toString());
                }
                return datasource.getConnection();
            } catch (IllegalAccessException | InvocationTargetException | SQLException ex) {
                logger.log(Level.SEVERE, null, ex);
                throw new ServiceException("Unable to connect..  Please check the configuration parameter");
            }
        }


        private void defaultProperties(PoolProperties p) {
            p.setDefaultAutoCommit(false);
            p.setJmxEnabled(false);
            p.setTestOnBorrow(true);
            p.setValidationQuery("SELECT 1");
            p.setTestOnReturn(false);
            p.setValidationInterval(30000);
            p.setTimeBetweenEvictionRunsMillis(30000);
            p.setMaxWait(10000);
            p.setRemoveAbandonedTimeout(600);
            p.setRemoveAbandoned(true);
        }


        void shutdown() {
            if (datasource != null && datasource instanceof DataSourceProxy) {
                ((DataSourceProxy) datasource).close(true);
                System.out.println("Closed datasource " + datasource.toString());
            }
            datasource = null;
        }
    }

    /**
     * Map of JDBCPools.
     */
    private static final Map<String, JDBCPool> jdbcPools = new HashMap<>();
    private static final Resource[] NO_RES = new Resource[0];


    /**
     * Shuts down all JDBC pools.
     */
    public static void shutdownJDBC() {
        synchronized (jdbcPools) {
            for (JDBCPool pool : jdbcPools.values()) {
                pool.shutdown();
            }
            jdbcPools.clear();
        }
    }


    /**
     * Get the architecture
     * @return
     */
    public static String getArch() {
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("bsd")) {
            os = "bsd";
        } else {
            os = os.substring(0, 3);
        }
        return os + "-" + System.getProperty("os.arch").toLowerCase();
    }


    private static Executable asExecutable(final Execution p1, final File workspace, final SessionLogger LOG) {

        return new Executable() {
            {
                try {
                    p1.redirectOutput(new FileWriter(stdout()));
                    p1.redirectError(new FileWriter(stderr()));
                } catch (IOException ex) {
                    LOG.log(Level.WARNING, null, ex);
                }
                p1.setWorkingDirectory(workspace);
                p1.setLogger(LOG);
            }


            @Override
            public File stdout() {
                return new File(workspace, p1.getFile().getName() + STDOUT);
            }


            @Override
            public File stderr() {
                return new File(workspace, p1.getFile().getName() + STDERR);
            }


            @Override
            public String getName() {
                return p1.getFile().toString();
            }


            @Override
            public void setArguments(Object... args) {
                List<Object> l = new ArrayList<>();
                Object[] o = p1.getArguments();
                if (o.length > 0 && o[0] instanceof String && p1.getFile().getName().contains("wine")) {
                    // keep the first argument, if execution is wine.
                    l.add(o[0]);
                }
                l.addAll(Arrays.asList(args));
                p1.setArguments(l);
            }


            @Override
            public void addArguments(Object... args) {
                List<Object> l = new ArrayList<>();
                l.addAll(Arrays.asList(p1.getArguments()));
                l.addAll(Arrays.asList(args));
                p1.setArguments(l);
            }


            @Override
            public Object[] getArguments() {
                return p1.getArguments();
            }


            @Override
            public Map<String, String> environment() {
                return p1.environment();
            }


            @Override
            public int exec() throws IOException {
                int ret = p1.exec();
                if (ret != 0 && LOG.isLoggable(Level.INFO) && stderr().exists()) {
                    String s = FileUtils.readFileToString(stderr());
                    LOG.severe(s);
                }
                return ret;
            }


            @Override
            public void redirectDefaults() throws IOException {
                p1.redirectOutput(new FileWriter(stdout(), true));
                p1.redirectError(new FileWriter(stderr(), true));
            }


            @Override
            public void redirectOutput(String file) throws IOException {
                if (file == null) {
                    throw new IOException("missing file name for redirect.");
                }
                p1.redirectOutput(new FileWriter(new File(workspace, file)));
            }


            @Override
            public void redirectError(String file) throws IOException {
                if (file == null) {
                    throw new IOException("missing file name for redirect.");
                }
                p1.redirectError(new FileWriter(new File(workspace, file)));
            }


            @Override
            public void redirectOutput(StringWriter w) throws IOException {
                p1.redirectOutput(w);
            }


            @Override
            public void redirectError(StringWriter w) throws IOException {
                p1.redirectError(w);
            }
        };
    }


    /**
     * Get a file resources.
     * @param c
     * @param id
     * @return
     */
    public static File getResourceFile(Class<?> c, String id) throws ServiceException {
        Resource resource = getResourceById(c, id);
        if (resource == null) {
            throw new ServiceException("No such resource: '" + id + "'");
        }
        String file = resource.file();
        if (file == null || file.isEmpty()) {
            return null;
        }
        File csip_home = new File(Config.getString("csip.dir"));
        file = replaceArch(file);
        if (file.endsWith("zip") && resource.type() == ResourceType.ARCHIVE) {
            return new File(csip_home, file.substring(0, file.indexOf('.')));
        }
        File f = new File(csip_home, file);
        if (!f.exists() || !f.canRead()) {
            throw new ServiceException("Cannot find file: '" + file + "'");
        }
        return f;
    }


    /**
     * Get the resource as java file.
     *
     * @param c
     * @param id
     * @param workspace
     * @param jars
     * @param LOG
     * @return
     * @throws ServiceException
     */
    public static Executable getResourceJava(Class<?> c, String id, final File workspace,
            List<File> jars, SessionLogger LOG) throws ServiceException {
        Resource r = getResourceById(c, id);
        if (r == null || r.type() != ResourceType.CLASSNAME) {
            throw new ServiceException("Not found: " + id);
        }

        String oms_java_home = Config.getString("oms.java.home", "/opt/jdk1.7.0_45");
        Execution p = new Execution(new File(oms_java_home, "/bin/java"));
        p.setArguments(//r[0].args().split("\\s+"), // no jvmoptions
                "-cp", Binaries.asClassPath(jars),
                r.file(),
                r.args().split("\\s+")
        );
        if (r.env().length > 0) {
            p.environment().putAll(parseEnv(r.env()));
        }
        return asExecutable(p, workspace, LOG);
    }


    /**
     *
     * @param c
     * @param id
     * @param workspace
     * @param jars
     * @param LOG
     * @return
     * @throws ServiceException
     */
    public static Executable getResourceOMSDSL(Class<?> c, String id,
            final File workspace, List<File> jars, SessionLogger LOG) throws ServiceException {
        Resource r = getResourceById(c, id);
        if (r == null || r.type() != ResourceType.OMS_DSL) {
            throw new ServiceException("Not found: " + id);
        }

        String oms_java_home = Config.getString("oms.java.home", "/opt/jdk1.7.0_45");
        Execution p = new Execution(new File(oms_java_home, "/bin/java"));
        p.setArguments(r.args().split("\\s+"), // jvmoptions
                "-cp", asClassPath(jars),
                "oms3.CLI",
                "-r", r.file()
        );

        if (r.env().length > 0) {
            p.environment().putAll(parseEnv(r.env()));
        }
        return asExecutable(p, workspace, LOG);
    }


    /**
     * Get the JDBC connection from a resource definition.
     *
     * @param c
     * @param id
     * @param LOG
     * @return
     * @throws ServiceException
     */
    public static Connection getRessourceJDBC(Class<?> c, String id, SessionLogger LOG) throws ServiceException {
        synchronized (jdbcPools) {
            JDBCPool p = jdbcPools.get(id);
            if (p == null) {
                Resource r = getResourceById(c, id);
                if (r != null) {
                    if (r.type() == ResourceType.JDBC) {
                        String url = r.file();
                        if (url != null && !url.isEmpty()) {
                            Map<String, String> env = parseEnv(r.env());
                            p = new JDBCPool(url, env);
                            jdbcPools.put(id, p);
                        } else {
                            throw new ServiceException("Not url connection string in 'file' " + id);
                        }
                    } else {
                        throw new ServiceException("Not a JDBC resource: " + id);
                    }
                } else {
                    throw new ServiceException("No such resource: " + id);
                }
            }
            return p.getConnection(LOG);
        }
    }


    public static void addToJDBCPool(String id, String url) {
        Map<String, String> env = new HashMap<>();
        jdbcPools.put(id, new JDBCPool(url, env));
    }


    public static Connection getConnection(String id, SessionLogger logger) throws ServiceException {
        JDBCPool p = jdbcPools.get(id);
        if (p == null) {
            throw new ServiceException("No such resource: " + id);
        }
        return p.getConnection(logger);
    }


    /**
     * Get a file resources.
     * @param c
     * @param id
     * @param workspace
     * @param LOG
     * @return
     */
    public static Executable getResourceExe0(Class<?> c, String id,
            final File workspace, SessionLogger LOG) throws ServiceException {

        Resource resource = getResourceById(c, id);
        if (resource == null) {
            throw new ServiceException("Not found: " + id);
        }

        String file = resource.file();
        if (file == null || file.isEmpty()) {
            return null;
        }

        file = replaceArch(file);
        File executable;
        if (resource.type() == ResourceType.REFERENCE) {
            executable = new File(file);
            // check only if there is a reference to an absolute file.
            // relative commads could use the PATH.
            if (executable.isAbsolute() && !executable.exists()) {
                throw new ServiceException("Referenced executable not found: " + file);
            }
            if (executable.isAbsolute() && !executable.canExecute()) {
                throw new ServiceException("Referenced executable not executable: " + file);
            }
        } else if (resource.type() == ResourceType.EXECUTABLE) {
            try {
                File csip_home = new File(Config.getString("csip.dir", "/tmp/csip"));
                executable = Binaries.unpackResource(file, csip_home);
            } catch (IOException ex) {
                throw new ServiceException(ex);
            }
        } else {
            throw new ServiceException("Illegal resource type: " + resource.type());
        }

        Execution p = null;
        List<Object> args = new ArrayList<>();
        if (resource.wine() && File.pathSeparatorChar == ':') {
            p = new Execution(new File(Config.getString("wine.path", "/usr/bin/wine")));
            args.add(executable.toString());
        } else {
            p = new Execution(new File(executable.toString()));
        }

        if (!resource.args().isEmpty()) {
            args.add(resource.args().split("\\s+"));
        }

        if (args.size() > 0) {
            p.setArguments(args);
        }

        if (resource.env().length > 0) {
            p.environment().putAll(parseEnv(resource.env()));
        }

        return asExecutable(p, workspace, LOG);
    }


    public static Resource[] getMergedResources(Class<?> c) {
        Resource[] r = c.getAnnotationsByType(Resource.class);
        if (r == null || r.length == 0) {
            return NO_RES;
        }
        List<Resource> l = new ArrayList<>();
        for (Resource res : r) {
            if (res.from() != Object.class) {
                Resource[] r1 = getMergedResources(res.from());
                l.addAll(Arrays.asList(r1));
            } else {
                l.add(res);
            }
        }
        return l.toArray(new Resource[l.size()]);
    }


    public static Resource[] getResourcesByType(Class<?> c, ResourceType type) {
        Resource[] r = getMergedResources(c);
        List<Resource> l = new ArrayList<>();
        for (Resource resource : r) {
            if (resource.type() == type) {
                l.add(resource);
            }
        }
        return l.toArray(new Resource[l.size()]);
    }


    public static Resource getResourceById(Class<?> c, String id) {
        for (Resource resource : getMergedResources(c)) {
            if (resource.id().equals(id)) {
                return resource;
            }
        }
        return null;
    }


    public static void extractAllResources(Class<?> c, SessionLogger LOG) throws ServiceException {
        File csip_home = new File(Config.getString("csip.dir", "/tmp/csip"));
        csip_home.mkdirs();
        for (Resource resource : getMergedResources(c)) {
            if (resource.type() == ResourceType.REFERENCE || resource.type() == ResourceType.JDBC || resource.id().equals("")) {
                continue;
            }
            String f = resource.file();
            if (f == null || f.isEmpty()) {
                continue;
            }
            f = replaceArch(f);
            try {
                if (!new File(csip_home, f).exists()) {
                    File file = Binaries.unpackResource(f, csip_home);
                    if (file.getName().endsWith("zip") && resource.type() == ResourceType.ARCHIVE) {
                        ZipFiles.unzip(file);
                    }
                }
            } catch (IOException ex) {
                LOG.log(Level.SEVERE, null, ex);
            }
        }
    }


    static String replaceArch(String s) {
        return s.replace("${arch}", getArch());
    }

    static private final long date_at_startup = new Date().getTime();


    /**
     *
     * @param name
     * @param file_or_folder
     * @return
     * @throws IOException
     */
    public static File unpackResource(String name, File file_or_folder) throws IOException {
        name = replaceArch(name);

        URL u = Binaries.class.getResource(name);
        if (u == null) {
            throw new IllegalArgumentException("No such resource " + name);
        }

        File outFile = null;
        if (file_or_folder.isDirectory()) {
            outFile = new File(file_or_folder, name);
        } else {
            outFile = file_or_folder;
        }

        URLConnection con = u.openConnection();
        long date = con.getHeaderFieldDate("Last-Modified", date_at_startup);

        // newer date?
        if (outFile.exists() && (date < outFile.lastModified())) {
            return outFile;
        }

        // different size?
        if (outFile.exists() && (con.getContentLengthLong() == outFile.length())) {
            return outFile;
        }

        if (!outFile.getParentFile().exists()) {
            outFile.getParentFile().mkdirs();
        }

        InputStream in = new BufferedInputStream(u.openStream());
        FileUtils.copyInputStreamToFile(in, outFile);
        in.close();

        outFile.setExecutable(true);
        outFile.setReadable(true);
        outFile.setLastModified(date);
        return outFile;
    }


    /**
     *
     * @param name
     * @param newName
     * @return
     * @throws IOException
     */
    public static File unpackResourceAbsolute(String name, String newName) throws IOException {
        name = replaceArch(name);
        URL u = Binaries.class.getResource(name);
        if (u == null) {
            throw new IllegalArgumentException("No such resource " + name);
        }

        File outFile = new File(newName);
        URLConnection con = u.openConnection();

        if (outFile.exists() && con.getDate() < outFile.lastModified()) {
            // the local file exists and is newer.
            return outFile;
        }

        if (outFile.exists() && con.getContentLength() == outFile.length()) {
            return outFile;
        }

        if (!outFile.getParentFile().exists()) {
            outFile.getParentFile().mkdirs();
        }

        InputStream in = new BufferedInputStream(u.openStream());
        FileUtils.copyInputStreamToFile(in, outFile);
        in.close();

        outFile.setExecutable(true);
        outFile.setLastModified(con.getDate());
        return outFile;
    }


    /**
     * get the FS usage of that directory/file location in percent.
     *
     * @param file
     * @return value 0.0 .. 1.0, 1.0 means 100% full.
     */
    public static double getFSUsage(File file) {
        return 1 - (double) file.getUsableSpace() / file.getTotalSpace();
    }


    /**
     * get the library jars
     *
     * @param dir
     * @return
     */
    public static List<File> getJars(File dir) {
        File[] f = dir.listFiles((FilenameFilter) new WildcardFileFilter("*.jar"));
        if (f == null) {
            return new ArrayList<>();
        }
        return Arrays.asList(f);
    }


    /**
     * Convert the files to a classpath
     *
     * @param f
     * @param jar file list
     * @return
     */
    public static String asClassPath(List<File> f) {
        StringBuilder b = new StringBuilder();
        for (int i = 0; i < f.size(); i++) {
            if (f.get(i).getName().endsWith(".jar")) {
                b.append(f.get(i).toString());
                if (i < f.size() - 1) {
                    b.append(File.pathSeparatorChar);
                }
            }
        }
        return b.toString();
    }


    /**
     * Converts a map to system properties.
     *
     * @param m
     * @return
     */
    public static String[] asSysProps(Map<String, String> m) {
        List<String> b = new ArrayList<>();
        for (String key : m.keySet()) {
            String val = m.get(key);
            b.add("-D" + key + "=" + val);
        }
        return b.toArray(new String[b.size()]);
    }


    /**
     *
     * @param env
     * @return
     */
    public static Map<String, String> parseEnv(String[] env) {
        Map<String, String> m = new HashMap<>();
        if (env == null) {
            return m;
        }
        for (String s : env) {
            if (s == null || s.isEmpty()) {
                continue;
            }
            String t[] = s.trim().split("\\s*=\\s*");
            if (t.length == 2) {
                m.put(t[0], resolve(t[1]));
            }
        }
        return m;
    }


    /**
     * Parses a string into a number of bytes. e.g. "1KB" -> 1024,
     * @param in
     * @return the size in bytes.
     */
    public static long parseByteSize(String in) {
        in = in.trim().replaceAll(",", ".");
        try {
            return Long.parseLong(in);
        } catch (NumberFormatException e) {
        }
        Matcher m = Pattern.compile("([\\d.,]+)\\s*(\\w)").matcher(in);
        m.find();
        double scale = 1;
        switch (m.group(2).charAt(0)) {
            case 'G':
            case 'g':
                scale *= 1024;
            case 'M':
            case 'm':
                scale *= 1024;
            case 'K':
            case 'k':
                scale *= 1024;
                break;
            default:
                throw new IllegalArgumentException(in);
        }
        return Math.round(Double.parseDouble(m.group(1)) * scale);
    }


    /**
     * Resolve a string with system and CSIP properties.
     *
     * @param str the string to resolve
     * @return the resolved string.
     */
    public static String resolve(String str) {
        if (!str.contains("${")) {
            return str;
        }
        Properties p = new Properties(System.getProperties());
        p.putAll(Config.properties());
        return resolve0(str, p);
    }


    /**
     * property substitution in a string.
     *
     * @param str
     * @return
     */
    private static String resolve0(String str, Properties p) {
        int idx = 0;
        while (idx < str.length()) {
            int start = str.indexOf("${", idx);
            int end = str.indexOf("}", idx);
            if (start == -1 || end == -1 || end < start) {
                break;
            }
            String key = str.substring(start + 2, end);
            String val = p.getProperty(key);
            int inc;
            if (val != null) {
                str = str.replace("${" + key + "}", val);
                inc = val.length();
            } else {
                inc = key.length() + 3;
            }
            idx = start + inc;
        }
        return str;
    }

    //        String a = "jdbc:sqlserver://129.82.20.242:1433;databaseName=lmod_ZedX;user=lmod-rw;password=managements";
//        String b = a.replaceAll("password=.+[;]?", "password=****;");
//
//        System.out.println(b);
//        String a = resolve("${csip.arch}  all to ${user.dir} is true. '${}'live in ${csip.dir}");
//        System.out.println(a);
//
////        File dir = new File("/tmp");
////        
////        String b = "odd/bcd/ab.txt";
//        File f = new File("odd/bcd/ab.txt");
//        f.getParentFile().mkdirs();
//
//        FileOutputStream os = new FileOutputStream(f);
//        os.write("olaf".getBytes());
//        os.close();
////        IOUtils.writeStringToFile(f, "hi there");
}