Services.java [src/csip/utils] Revision: 54532d51c31186ee4a25258c2c3c643366480666  Date: Mon Apr 10 12:02:14 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.utils;

import csip.Config;
import csip.ModelDataService;
import csip.ServiceException;
import csip.SessionLogger;
import java.io.*;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.ws.rs.Path;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.io.FileUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.glassfish.jersey.media.multipart.BodyPart;
import org.glassfish.jersey.media.multipart.BodyPartEntity;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;

/**
 * service utilities.
 *
 * @author Olaf David
 */
public class Services {

    public static final String LOCAL_IP_ADDR = getLocalIP();
    public static final int ENSEMBLE_THREADS = 10;

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


    /**
     * Returns the current local IP address or an empty string in error case /
     * when no network connection is up.
     *
     * @return Returns the current local IP address or an empty string in error
     * case.
     * @since 0.1.0
     */
    private static String getLocalIP() {

        String ipOnly = "";
        try {
            Enumeration<NetworkInterface> nifs = NetworkInterface.getNetworkInterfaces();
            if (nifs == null) {
                return "";
            }
            while (nifs.hasMoreElements()) {
                NetworkInterface nif = nifs.nextElement();
                if (!nif.isLoopback() && nif.isUp() && !nif.isVirtual()) {
                    Enumeration<InetAddress> adrs = nif.getInetAddresses();
                    while (adrs.hasMoreElements()) {
                        InetAddress adr = adrs.nextElement();
                        if (adr != null && !adr.isLoopbackAddress() && (nif.isPointToPoint() || !adr.isLinkLocalAddress())) {
                            String adrIP = adr.getHostAddress();
                            String adrName = nif.isPointToPoint() ? adrIP : adr.getCanonicalHostName();
                            if (!adrName.equals(adrIP)) {
                                return adrIP;
                            } else {
                                ipOnly = adrIP;
                            }
                        }
                    }
                }
            }
            if (ipOnly.length() == 0) {
                return null;
            }
            return ipOnly;
        } catch (SocketException ex) {
            return null;
        }
    }


    static {
        Calendar uuidEpoch = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        uuidEpoch.clear();
        uuidEpoch.set(1582, 9, 15, 0, 0, 0); // 9 = October
        epochMillis = uuidEpoch.getTime().getTime();
    }

    static long epochMillis;
//    

//    private static long getTime(UUID uuid) {
//        return (uuid.timestamp() / 10000L) + epochMillis;
//    }
//    static long getTime(String uuid) {
//        UUID u = UUID.fromString(uuid);
//        return (u.timestamp() / 10000L) + epochMillis;
//    }
    static SimpleDateFormat f = new SimpleDateFormat("/dd/HH");


    private static synchronized String getPrefix(String uuid) {
        UUID u = UUID.fromString(uuid);
        long time = (u.timestamp() / 10000L) + epochMillis;
        return f.format(new Date(time));
    }

//    public static synchronized String getPrefix(UUID uuid) {
//        SimpleDateFormat f = new SimpleDateFormat("/dd/HH");
//        return f.format(new Date(getTime(uuid)));
//    }

    public static File getResultsDir(String suid) {
        return new File(Config.getString("csip.results.dir", "/tmp/csip/results") + getPrefix(suid), suid);
    }


    public static File getWorkDir(String suid) {
        return new File(Config.getString("csip.work.dir", "/tmp/csip/work") + getPrefix(suid), suid);
    }


//    public static void main(String[] args) {
//        
//        System.out.println(getPrefix("6b7a63c1-16b6-11e5-83a9-112c53ec8f43"));
////        TimeBasedGenerator gen = Generators.timeBasedGenerator();
////        UUID i = gen.generate();
////        System.out.println(getTime(i));
////        System.out.println(new Date(getTime(i)));
//    }
    /**
     * Create a dummy callable
     *
     * @return a callable that does nothing.
     * @throws Exception
     */
    public static Callable<String> dummyCallable() throws Exception {
        return new Callable<String>() {
            @Override
            public String call() throws Exception {
                return ModelDataService.EXEC_OK;
            }
        };
    }

    public static class FormDataParameter {

        String name;
        InputStream is;
        String filename;
        String value;


        public FormDataParameter(BodyPart bp) {
            FormDataContentDisposition fd = (FormDataContentDisposition) bp.getContentDisposition();
            name = fd.getName();
            if (fd.getFileName() != null) {
                BodyPartEntity bpe = (BodyPartEntity) bp.getEntity();
                is = bpe.getInputStream();
                filename = fd.getFileName();
            } else {
                value = bp.getEntityAs(String.class);
            }
        }


        public InputStream getInputStream() {
            return is;
        }


        public boolean isFile() {
            return filename != null;
        }


        public String getValue() {
            return value;
        }


        public String getFilename() {
            return filename;
        }


        public String getName() {
            return name;
        }
    }


    /**
     * Creates a map of Strings pointing to the input streams fo files
     *
     * @param b
     * @return The form parameter
     */
    public static Map<String, FormDataParameter> getFormParameter(List<BodyPart> b) {
        Map<String, FormDataParameter> m = new HashMap<>();
        for (BodyPart bp : b) {
            FormDataContentDisposition fd = (FormDataContentDisposition) bp.getContentDisposition();
            m.put(fd.getName(), new FormDataParameter(bp));
        }
        return m;
    }


    @Deprecated
    public static File[] toFiles(String... n) {
        File[] f = new File[n.length];
        for (int i = 0; i < f.length; i++) {
            f[i] = new File(n[i]);
        }
        return f;
    }


    public static String replaceHostinURI(URI uri, String newHost) throws Exception {
//        String port = Config.getString("vm.port", "8080");
//        return "http://" + newHost + ":" + port + uri.toURL().getPath();
        int port = Config.getInt("csip.peer.port", 8080);
        UriBuilder b = UriBuilder.fromUri(uri);
        URI u = b.host(newHost).port(port).build();
        LOG.info("replace in Host: " + u.toString());
        return u.toString();
    }


    public static URI toPublicURL(URI u) {
        UriBuilder b = UriBuilder.fromUri(u);
        String s = Config.getString("csip.public.scheme");
        if (s != null) {
            b = b.scheme(s);
        }
        s = Config.getString("csip.public.host");
        if (s != null) {
            b = b.host(s);
        }
        s = Config.getString("csip.public.port");
        if (s != null) {
            b = b.port(Integer.parseInt(s));
        }
        return b.build();
    }


    /**
     * Copy the formParameter (files) to a directory, if the file is an archive,
     * The content will be extracted.
     *
     * @param log
     * @param dir
     * @param forms
     * @return the extracted files.
     * @throws ServiceException
     */
    public static String[] copyAndExtract(SessionLogger log, File dir, Map<String, FormDataParameter> forms, boolean unpack) throws ServiceException {
        List<String> files = new ArrayList<>();
        for (FormDataParameter fd : forms.values()) {
            if (!fd.isFile()) {
                continue;
            }
            String name = fd.getFilename();
            name = name.replace('\\', '/');
//            name = name.substring(name.lastIndexOf('/') + 1);  // TODo why?

            InputStream fis = fd.getInputStream();
            String lcName = name.toLowerCase();

            // archives
            try {
                if ((lcName.endsWith(".bz2") || lcName.endsWith(".gz")) && unpack) {
                    // wrapper (supports single files, as well as tar.gz / tar.bzls
                    fis = new CompressorStreamFactory().createCompressorInputStream(new BufferedInputStream(fis));
                    name = removeExt(name);
                    lcName = name.toLowerCase();
                }
                if ((lcName.endsWith(".zip") || lcName.endsWith(".tar")) && unpack) {
                    ArchiveInputStream is = new ArchiveStreamFactory().createArchiveInputStream(new BufferedInputStream(fis));
                    ArchiveEntry entry = null;
                    while ((entry = is.getNextEntry()) != null) {
                        if (is.canReadEntryData(entry)) {
                            if (entry.isDirectory()) {
                                new File(dir, entry.getName()).mkdirs();
                            } else {
                                File f = new File(dir, entry.getName());
                                if (!f.getParentFile().exists()) {
                                    f.getParentFile().mkdirs();
                                }
                                try (FileOutputStream ous = new FileOutputStream(f)) {
                                    copyAndCheckFS(is, ous);
                                }
                                files.add(entry.getName());
                                f.setLastModified(entry.getLastModifiedDate().getTime());
                                log.info("Extracted :" + entry.getName() + " as " + f);
                            }
                        }
                    }
                    is.close();
                } else {
                    File f = new File(dir, name);
                    if (!f.getParentFile().exists()) {
                        f.getParentFile().mkdirs();
                    }
                    try (FileOutputStream ous = new FileOutputStream(f)) {
                        copyAndCheckFS(fis, ous);
                    }
                    files.add(name);
                    if (log.isLoggable(Level.INFO)) {
                        log.info("copy form data file: " + name + " to " + dir);
                    }
                }
            } catch (CompressorException | ArchiveException ex) {
                // should not happen
            } catch (IOException ex) {
                throw new ServiceException(ex);
            }
        }
        return files.toArray(new String[files.size()]);
    }


    public static void copyAndCheckFS(InputStream input, OutputStream output) throws IOException {
        byte[] buffer = new byte[4096];
        int n = 0;
        // 85% is full !
//        double fsFull = Config.getDouble("m.fsfull", 0.85);
        while ((n = input.read(buffer)) != -1) {
//            if (BinUtils.getFSUsage(dir) > fsFull) {
//                throw new IOException("File system full.");
//            }
            output.write(buffer, 0, n);
        }
    }


    public static String removeExt(String name) {
        return name.substring(0, name.lastIndexOf("."));
    }


    public static String removeFirstLastChar(String text) {
        return text.substring(1, text.length() - 1);
    }


    public static String md5(File file) {
        if (!file.exists()) {
            throw new IllegalArgumentException("not found: " + file);
        }
        if (file.length() == 0) {
            throw new IllegalArgumentException("empty file: " + file);
        }
        FileInputStream fIn = null;
        try {
            fIn = new FileInputStream(file);
            MessageDigest md = MessageDigest.getInstance("MD5");
            FileChannel fChan = fIn.getChannel();
            ByteBuffer mBuf = ByteBuffer.allocate((int) fChan.size());
            fChan.read(mBuf);
            return toHex(md.digest(mBuf.array()));
        } catch (Exception ex) {
            ex.printStackTrace(System.err);
        } finally {
            try {
                if (fIn != null) {
                    fIn.close();
                }
            } catch (IOException ex) {
            }
        }
        return "";
    }

//////////////////////////////
// private
/////////////////////////////

    private static String toHex(byte[] data) {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < data.length; i++) {
            int halfbyte = (data[i] >>> 4) & 0x0F;
            int two_halfs = 0;
            do {
                if ((0 <= halfbyte) && (halfbyte <= 9)) {
                    buf.append((char) ('0' + halfbyte));
                } else {
                    buf.append((char) ('a' + (halfbyte - 10)));
                }
                halfbyte = data[i] & 0x0F;
            } while (two_halfs++ < 1);
        }
        return buf.toString();
    }

    public interface CallableFactory {

        Callable create(int i);
    }


//    public static List<Future<JSONObject>> run(int max, final CallableFactory callable) {
//        return run(max, Config.getInt("codebase.threadpool", ENSEMBLE_THREADS), callable);
//    }
//
//
//    public static List<Future<JSONObject>> run(int max, int threads, final CallableFactory callable) {
//        final CountDownLatch latch = new CountDownLatch(max);
//        final int attempts = 3;
//        final boolean fail_all = true;
//
//        final ExecutorService executor = Executors.newFixedThreadPool(threads);
//        List<Future<JSONObject>> resp = new ArrayList<>();
//        for (int i = 0; i < max; i++) {
//            final int ii = i;
//            resp.add(executor.submit(new Callable<JSONObject>() {
//                @Override
//                public JSONObject call() throws Exception {
//                    JSONObject o = null;
//                    String err_msg = null;
//                    Callable<JSONObject> ca = callable.create(ii);
//                    int a = attempts;
//                    // allow trying this multiple times.
//                    while (a-- > 0 && o == null) {
//                        try {
//                            o = ca.call();
//                        } catch (Exception E) {
//                            err_msg = E.getMessage();
//                        }
//                    }
//                    if (o == null && fail_all) {
//                        executor.shutdown();
//                        throw new ServiceException("Failed service :" + ii + " " + err_msg);
//                    }
//                    latch.countDown();
//                    return o == null ? JSONUtils.error(err_msg) : o;
//                }
//            }));
//        }
//        try {
//            latch.await();
//        } catch (InterruptedException ex) {
//        }
//        executor.shutdown();
//        return resp;
//    }
    static synchronized ExecutorService getES(int nthreads, int bq_len) {
        BlockingQueue<Runnable> bq = new ArrayBlockingQueue<>(nthreads + bq_len);
        RejectedExecutionHandler eh = new ThreadPoolExecutor.CallerRunsPolicy();
        ExecutorService es = new ThreadPoolExecutor(nthreads, nthreads, 0L, TimeUnit.MILLISECONDS, bq, eh);
        return es;
    }


    public static void runParallel(int count, CallableFactory factory) {
        runParallel(count, Config.getInt("csip.service.peers", 4), factory);
    }


    public static void runParallel(int count, int threads, CallableFactory factory) {
        runParallel(count, threads, Config.getInt("csip.internal.call.attempts", 4),
                Config.getInt("csip.internal.bq", 4), factory);
    }


    public static void runParallel(int count, int threads, final int attempts, int bq, CallableFactory factory) {

        // have the number of threads being bound by count
        int threads_ = Math.min(count, threads);
        final ExecutorService exec = getES(threads_, bq);
        final CountDownLatch latch = new CountDownLatch(count);
        for (int i = 0; i < count; i++) {
            final Callable c = factory.create(i);
            exec.submit(new Runnable() {
                @Override
                public void run() {
                    int a = attempts;
                    Exception Ex = null;
                    while (a > 0) {
                        try {
                            c.call();
                            break;
                        } catch (Exception E) {
                            System.err.println("Failed #" + a);
                            Ex = E;
                            a--;
                        }
                    }
                    if (Ex != null) {
                        System.err.println("Failed all attempts, last exception:");
                        Ex.printStackTrace(System.err);
                        exec.shutdownNow();
                    }
                    latch.countDown();
                }
            });
        }
        try {
            latch.await();
        } catch (InterruptedException ex) {
        }
        exec.shutdownNow();
    }


//    public static List<Future<JSONObject>> run0(int max, int threads, final CallableFactory callable) {
//        final CountDownLatch latch = new CountDownLatch(max);
//        final ExecutorService executor = Executors.newFixedThreadPool(threads);
//        List<Future<JSONObject>> resp = new ArrayList<>();
//        for (int i = 0; i < max; i++) {
//            final int ii = i;
//            resp.add(executor.submit(new Callable<JSONObject>() {
//                @Override
//                public JSONObject call() throws Exception {
//                    JSONObject o = null;
//                    String err_msg = null;
//                    Callable<JSONObject> ca = callable.create(ii);
//                    try {
//                        o = ca.call();
//                    } catch (Exception E) {
//                        err_msg = E.getMessage();
//                    } catch (AssertionError E) {
//                        err_msg = E.getMessage();
//                    }
//                    latch.countDown();
//                    return (err_msg != null) ? JSONUtils.error(err_msg) : o;
//                }
//            }));
//        }
//        try {
//            latch.await();
//        } catch (InterruptedException ex) {
//        }
//        executor.shutdownNow();
//        return resp;
//    }
    /**
     * run all models at once.
     *
     * @param models
     * @return the list of futures
     * @throws ExecutionException
     */
    public static List<Future<JSONObject>> runEnsemble(List<Callable<JSONObject>> models) throws ExecutionException {

        final ExecutorService executor = Executors.newFixedThreadPool(Config.getInt("codebase.threadpool", ENSEMBLE_THREADS));
        final CountDownLatch barrier = new CountDownLatch(models.size());
        final List<Future<JSONObject>> results = new ArrayList<>();
        // Model callables
        for (final Callable<JSONObject> ca : models) {
            results.add(executor.submit(new Callable<JSONObject>() {
                @Override
                public JSONObject call() {
                    JSONObject res = null;
                    try {
                        res = ca.call();
                    } catch (Exception E) {
                        executor.shutdownNow();
                    }
                    barrier.countDown();
                    return res;
                }
            }));
        }

        try {
            barrier.await();
        } catch (InterruptedException E) {
        }

        executor.shutdown();
        return results;
    }


    /**
     * Slice a original request into single runs.
     *
     * @param req
     * @param path
     * @return the mapped list of ensembles.
     */
    public static List<Callable<JSONObject>> mapEnsemble(JSONObject req, String path) throws JSONException {

        String codebase = Config.getString("codebase.url", "http://csip.engr.colostate.edu:8081/rest");

        JSONObject metainfo = req.getJSONObject(ModelDataService.KEY_METAINFO);
        if (!req.has(ModelDataService.KEY_METAINFO) || !metainfo.has(ModelDataService.KEY_PARAMETERSETS)) {
            return null;
        }
        List<Callable<JSONObject>> runs = new ArrayList<Callable<JSONObject>>();
        if (metainfo.has(ModelDataService.KEY_PARAMETERSETS)) {
            JSONArray psets = req.getJSONArray(ModelDataService.KEY_PARAMETER);
            for (int i = 0; i < metainfo.getInt(ModelDataService.KEY_PARAMETERSETS); i++) {
                JSONArray pset = psets.getJSONArray(i);
                JSONObject single_req = JSONUtils.newRequest(pset, new JSONObject());
                RestCallable mv = new RestCallable(single_req, codebase + path);
                runs.add(mv);
            }
        }
        return runs;
    }


    static boolean isFailed(JSONObject res) throws JSONException {
        return res.getJSONObject(ModelDataService.KEY_METAINFO).getString(ModelDataService.KEY_STATUS).equals("Failed");
    }


    public static JSONObject reduceEnsemble(List<Future<JSONObject>> ens, JSONObject orig_req) throws Exception {
        JSONArray results = new JSONArray();
        for (Future<JSONObject> future : ens) {
            JSONObject res = future.get();
            if (isFailed(res)) {
                orig_req.getJSONObject(ModelDataService.KEY_METAINFO).put(ModelDataService.KEY_STATUS, "Failed");
            }
            results.put(res.get(ModelDataService.KEY_RESULT));
        }
        return JSONUtils.newResponse(orig_req.getJSONArray(ModelDataService.KEY_PARAMETER), results, orig_req.getJSONObject(ModelDataService.KEY_METAINFO));
    }

    static public class RestCallable implements Callable<JSONObject> {

        JSONObject req;
        String url;


        public RestCallable(JSONObject req, String url) {
            this.req = req;
            this.url = url;
        }


        @Override
        public JSONObject call() throws Exception {
            Client client = ClientBuilder.newClient();
            WebTarget service = client.target(UriBuilder.fromUri(url).build());
            return service.request(MediaType.APPLICATION_JSON).post(Entity.json(req), JSONObject.class);
        }
    }


    /**
     * get the a context peer file
     *
     * @param e
     * @param ext
     * @return
     */
    public static File getContextFile(ServletContext e, String ext) {
        String s = e.getRealPath("/WEB-INF");
        if (s != null) {
            File f = new File(s).getParentFile();
            if (f != null && f.exists()) {
                File c = new File(f.getParentFile(), f.getName() + ext);
                if (c.exists() && c.canRead()) {
                    return c;
                }
            }
        }
        return null;
    }


    public static void filterServiceResources(ServletContext c, Set<Class<?>> orig) {
        File serviceList = getContextFile(c, ".services");
        // List to filter on.
        if (serviceList == null) {
            LOG.info("not found: " + serviceList + ", using all services.");
            return;
        }
        try {
            // read the file, a service for a line.
            List<String> srv = FileUtils.readLines(serviceList, "utf-8");
            LOG.info("Register Services from: " + serviceList);

            // create a lookup table
            Map<String, Class<?>> smap = new HashMap<>();
            for (Class<?> cl : orig) {
                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(org.glassfish.jersey.media.multipart.MultiPartFeature.class);
            for (Class<?> cl : orig) {
                if (cl.getCanonicalName().startsWith("csip.")) {
                    filtered_resources.add(cl);
                }
            }
            int flen = filtered_resources.size();
            for (String s : srv) {
                // allow for comment lines and empty lines.
                if (!s.isEmpty() && !s.trim().startsWith("#")) {
                    String service = s.trim();
                    // be a bit more tolerant
                    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 " + serviceList + ": '" + s.trim() + "', ignoring.");
                    }
                }
            }
            if (flen == filtered_resources.size()) {
                LOG.warning("No model or data service was enabled !!!!, check the file: ");
            }
            orig.clear();
            orig.addAll(filtered_resources);
        } catch (IOException ex) {
            LOG.log(Level.WARNING, "cannot read: " + serviceList);
            LOG.info("Using all services.");
        }
    }

}