All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.integratedmodelling.kserver.controller.KServerController Maven / Gradle / Ivy

There is a newer version: 0.9.10
Show newest version
/*******************************************************************************
 * Copyright (C) 2007, 2015:
 * 
 * - Ferdinando Villa  - integratedmodelling.org - any
 * other authors listed in @author annotations
 *
 * All rights reserved. This file is part of the k.LAB software suite, meant to enable
 * modular, collaborative, integrated development of interoperable data and model
 * components. For details, see http://integratedmodelling.org.
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms
 * of the Affero General Public License Version 3 or any later version.
 *
 * 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the Affero General Public License along with this
 * program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite
 * 330, Boston, MA 02111-1307, USA. The license is also available at:
 * https://www.gnu.org/licenses/agpl.html
 *******************************************************************************/
package org.integratedmodelling.kserver.controller;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;

import javax.imageio.ImageIO;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ImageWriterSpi;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;
import org.geotools.image.io.ImageIOExt;
import org.integratedmodelling.api.auth.IUser;
import org.integratedmodelling.api.configuration.IConfiguration;
import org.integratedmodelling.api.engine.IModelingEngine;
import org.integratedmodelling.api.network.API;
import org.integratedmodelling.api.network.IComponent;
import org.integratedmodelling.api.project.IProject;
import org.integratedmodelling.api.services.IPrototype;
import org.integratedmodelling.common.auth.User;
import org.integratedmodelling.common.beans.Service;
import org.integratedmodelling.common.beans.responses.Capabilities;
import org.integratedmodelling.common.beans.responses.Directory;
import org.integratedmodelling.common.beans.responses.FileResource;
import org.integratedmodelling.common.configuration.KLAB;
import org.integratedmodelling.common.configuration.KLAB.BootMode;
import org.integratedmodelling.common.resources.ResourceFactory;
import org.integratedmodelling.common.utils.FileUtils;
import org.integratedmodelling.common.utils.IPUtils;
import org.integratedmodelling.common.utils.URLUtils;
import org.integratedmodelling.engine.ModelingEngine;
import org.integratedmodelling.engine.NodeEngine;
import org.integratedmodelling.engine.modelling.kbox.ModelKbox;
import org.integratedmodelling.engine.modelling.kbox.ObservationKbox;
import org.integratedmodelling.exceptions.KlabAuthorizationException;
import org.integratedmodelling.exceptions.KlabIOException;
import org.integratedmodelling.kserver.Configuration;
import org.integratedmodelling.kserver.controller.components.UserManager;
import org.integratedmodelling.kserver.resources.services.DirectoryService;
import org.integratedmodelling.kserver.resources.services.FileService;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;

/**
 * Controller that allows to boot and access the k.LAB engine in both node and
 * modeling engine modes. Provides the public capabilities endpoint.
 * 
 * @author ferdinando.villa
 */
@RestController
public class KServerController {

    private final static double  DEFAULT_COMPARISON_TOLERANCE = 1e-9;

    @Autowired
    RequestMappingHandlerMapping handlerMapping;

    @Autowired
    ApplicationContext           appContext;

    @Autowired
    UserManager                  userManager;

    private static Log           logger                       = LogFactory.getLog(KServerController.class);

    @Value("${server.port}")
    private int                  port;

    /**
     * True when the client and the engine share the filesystem, so there is no need
     * for web transfer of assets.
     */
    private String               launchingClientSignature     = null;

    /**
     * Extract a set of resources from the classpath to the filesystem,
     * recursively.
     * 
     * @param rootPath
     * @param destination
     * @throws Exception
     */
    public static void extract(String rootPath, File destination) throws Exception {

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(rootPath + "/**");

        for (Resource resource : resources) {

            String path = null;
            if (resource instanceof FileSystemResource) {
                path = ((FileSystemResource) resource).getPath();
            } else if (resource instanceof ClassPathResource) {
                path = ((ClassPathResource) resource).getPath();
            }
            if (path == null) {
                throw new KlabIOException("internal: cannot establish path for resource " + resource);
            }

            String filePath = path.substring(path.indexOf(rootPath + "/") + rootPath.length() + 1);
            int pind = filePath.lastIndexOf('/');
            if (pind >= 0) {
                String fileDir = filePath.substring(0, pind);
                File destDir = new File(destination + File.separator + fileDir);
                destDir.mkdirs();
            }

            File dest = new File(destination + File.separator + filePath);
            try (InputStream is = resource.getInputStream()) {
                FileUtils.copyInputStreamToFile(is, dest);
            } catch (Exception e) {
                /*
                 * FIXME I can't find a way to determine if a resource
                 * corresponds to a directory, other than trying to extract as
                 * an inputstream and ignoring the exception (which can be both
                 * IOException and FileNotFoundException, thrown in different
                 * circumstances and even depending on shell substitution of
                 * path separators in same OS). This will silently ignore errors
                 * with other causes.
                 */
            }
        }
    }

    /*
     * extract core knowledge to a directory. Should obviously be generalized,
     * but will become unnecessary as we can use
     * /get/directory/im:ks:core.knowledge for the same purposes.
     * The code below will also insert beans in the correspondent catalogs, so
     * we can just remove the file extraction when it's time.
     */
    @SuppressWarnings("javadoc")
    public static void extractKnowledge() throws Exception {

        File kdir = KLAB.CONFIG.getDataPath("knowledge");
        File sdir = KLAB.CONFIG.getDataPath("ssh");
        File rcon = new File(KLAB.CONFIG.getDataPath() + File.separator + "access.properties");

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("/knowledge/**");
        Resource[] keyring = resolver.getResources("/ssh/pubring.gpg");

        if (!rcon.exists()) {
            URL prototype = null;
            try {
                prototype = resolver.getResource("classpath:access.properties.prototype").getURL();
                URLUtils.copy(prototype, rcon);
            } catch (IOException e) {
                // just don't
            }
        }

        if (keyring.length > 0) {
            try (InputStream in = keyring[0].getURL().openStream()) {
                FileUtils.copyInputStreamToFile(in, new File(sdir + File.separator + "pubring.gpg"));
            } catch (Exception e) {
                throw new KlabIOException(e);
            }
        }

        Directory directory = new Directory();

        for (Resource resource : resources) {

            String path = null;
            if (resource instanceof FileSystemResource) {
                path = ((FileSystemResource) resource).getPath();
            } else if (resource instanceof ClassPathResource) {
                path = ((ClassPathResource) resource).getPath();
            }
            if (path == null) {
                throw new KlabIOException("internal: cannot establish path for resource " + resource);
            }

            if (!path.endsWith("owl")) {
                continue;
            }

            String filePath = path.substring(path.indexOf("knowledge/") + "knowledge/".length());
            directory.getResourceUrns().add(API.CORE_KNOWLEDGE_URN + "#" + filePath);

            int pind = filePath.lastIndexOf('/');
            if (pind >= 0) {
                String fileDir = filePath.substring(0, pind);
                File destDir = new File(kdir + File.separator + fileDir);
                destDir.mkdirs();
            }
            File dest = new File(kdir + File.separator + filePath);
            InputStream is = resource.getInputStream();
            FileUtils.copyInputStreamToFile(is, dest);
            is.close();
        }

        DirectoryService.predefinedDirectories.put(API.CORE_KNOWLEDGE_URN, directory);
        DirectoryService.publishedDirectories.put(API.CORE_KNOWLEDGE_URN, kdir);
        FileService.publishedResources.put(API.CORE_PUBKEY_URN, FileResource
                .newFromClasspath("ssh/pubring.gpg", API.CORE_PUBKEY_URN, new Date().getTime()));
    }

    /**
     * Perform all initial setup tasks and calls boot() on the desired kind of
     * engine.
     * 
     * @param mode
     *            public node or modeling engine.
     * @return true if setup and boot succeeded.
     */
    public static boolean setup(BootMode mode) {

        /*
         * I'd rather not do this, but 100M of debug output when nobody has ever
         * asked for it and no property files are around anywhere are a bit much
         * to take.
         */
        Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        if (root != null) {
            root.setLevel(Level.INFO);
        }

        setupExtensions();
        KLAB.setLogger(logger);
        if (System.getProperty(IConfiguration.KLAB_DATA_DIRECTORY_PROPERTY) == null) {
            System.setProperty(IConfiguration.KLAB_DATA_DIRECTORY_PROPERTY, Configuration
                    .getApplicationPath(mode));
        }
        KLAB.CONFIG = new org.integratedmodelling.common.configuration.Configuration(false);

        /**
         * If we have the test.assets=xxx property, extract the named test
         * configuration to the data path.
         */
        String testAssets = System.getProperty(Configuration.TEST_ASSETS_PROPERTY);
        if (testAssets != null) {

            KLAB.info("test configuration " + testAssets + " requested: extracting assets to "
                    + KLAB.CONFIG.getDataPath());

            /*
             * substitute data directory with contents of asset dir
             */
            try {
                File datadir = KLAB.CONFIG.getDataPath();
                FileUtils.deleteDirectory(datadir);
                datadir.mkdirs();
                extract("test.assets/" + testAssets, datadir);
            } catch (Exception e) {
                KLAB.error(e);
                return false;
            }
        }

        try {
            extractKnowledge();
            if (mode == BootMode.MODELER) {
                ModelingEngine.boot();
            } else if (mode == BootMode.NODE) {
                NodeEngine.boot();
            }
        } catch (Exception e) {
            KLAB.error(e);
            return false;
        }

        return true;
    }

    /**
     * If this is not null, a client that passes the same signature can use
     * {@link API#DEPLOY}, {@link API#DELETE_NAMESPACE} and
     * {@link API#UPDATE_NAMESPACE} to work with local files instead of using the web.
     * 
     * @return
     */
    public String getLocalClientSignature() {

        if (this.launchingClientSignature == null) {

            this.launchingClientSignature = "no, sorry";

            File f = new File(KLAB.CONFIG.getDataPath() + File.separator + ".clientsig");
            if (f.exists()) {
                try {
                    this.launchingClientSignature = FileUtils.readFileToString(f);
                } catch (IOException e) {
                    // just use the web
                }
            }
        }

        return this.launchingClientSignature;
    }

    /*
     * Get extensions (ImageIO, Geotools) optimally configured. Most copied from
     * Geoserver's GeoserverInitStartupListener
     */
    private static void setupExtensions() {

        // if the server admin did not set it up otherwise, force X/Y axis
        // ordering
        // This one is a good place because we need to initialize this property
        // before any other opeation can trigger the initialization of the CRS
        // subsystem
        if (System.getProperty("org.geotools.referencing.forceXY") == null) {
            System.setProperty("org.geotools.referencing.forceXY", "true");
        }
        if (Boolean.TRUE.equals(Hints.getSystemDefault(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER))) {
            Hints.putSystemDefault(Hints.FORCE_AXIS_ORDER_HONORING, "http");
        }
        Hints.putSystemDefault(Hints.LENIENT_DATUM_SHIFT, true);

        // setup the referencing tolerance to make it more tolerant to tiny
        // differences
        // between projections (increases the chance of matching a random prj
        // file content
        // to an actual EPSG code
        double comparisonTolerance = DEFAULT_COMPARISON_TOLERANCE;

        // Register logging, and bridge to JAI logging
        GeoTools.init((Hints) null);

        /*
         * TODO make this a property and implement if it ever becomes necessary
         */
        // if (comparisonToleranceProperty != null) {
        // try {
        // comparisonTolerance =
        // Double.parseDouble(comparisonToleranceProperty);
        // } catch (NumberFormatException nfe) {
        // KLAB.warn("Unable to parse the specified COMPARISON_TOLERANCE "
        // + "system property: " + comparisonToleranceProperty +
        // " which should be a number. Using Default: " +
        // DEFAULT_COMPARISON_TOLERANCE);
        // }
        // }
        Hints.putSystemDefault(Hints.COMPARISON_TOLERANCE, comparisonTolerance);

        /*
         * avoid expiration of EPSG data. FIXME: does not seem to avoid
         * anything.
         */
        System.setProperty("org.geotools.epsg.factory.timeout", "-1");

        /*
         * Prevents leak ()
         */
        ImageIO.scanForPlugins();

        // in any case, the native png reader is worse than the pure java ones,
        // so
        // let's disable it (the native png writer is on the other side
        // faster)...
        ImageIOExt.allowNativeCodec("png", ImageReaderSpi.class, false);
        ImageIOExt.allowNativeCodec("png", ImageWriterSpi.class, true);

        // initialize GeoTools factories so that we don't make a SPI lookup
        // every time a
        // factory is needed
        Hints.putSystemDefault(Hints.FILTER_FACTORY, CommonFactoryFinder.getFilterFactory2(null));
        Hints.putSystemDefault(Hints.STYLE_FACTORY, CommonFactoryFinder.getStyleFactory(null));
        Hints.putSystemDefault(Hints.FEATURE_FACTORY, CommonFactoryFinder.getFeatureFactory(null));

        final Hints defHints = GeoTools.getDefaultHints();

        // Initialize GridCoverageFactory so that we don't make a lookup every
        // time a
        // factory is needed
        Hints.putSystemDefault(Hints.GRID_COVERAGE_FACTORY, CoverageFactoryFinder
                .getGridCoverageFactory(defHints));
    }

    /**
     * @return the port
     */
    public int getPort() {
        return port;
    }

    /**
     * Get a capabilities bean. List of capabilities is unrestricted even if the
     * services may be.
     * 
     * @param headers
     * @param request
     * @return capabilities
     */
    @RequestMapping(value = API.CAPABILITIES, method = RequestMethod.GET)
    public Capabilities capabilities(@RequestHeader HttpHeaders headers, HttpServletRequest request) {

        IUser user = userManager.getUser(headers.get(API.AUTHENTICATION_HEADER));

        Capabilities ret = new Capabilities();

        ret.setName(KLAB.NAME);

        /**
         * TODO publish services (from components etc). We could check for the
         * namespace of the method providers and attribute REST service
         * prototypes to components.
         */
        for (RequestMappingInfo hm : this.handlerMapping.getHandlerMethods().keySet()) {
            // System.out.println("" + hm);
        }

        /*
         * publish allowed components
         */
        for (IComponent c : KLAB.PMANAGER.getComponents()) {
            if (KLAB.ENGINE.getResourceConfiguration().isAuthorized(c, user, request.getRemoteAddr())) {
                ret.getComponentUrns().add(ResourceFactory.getComponentUrn(c));
            }
        }

        /**
         * publish function prototypes from all components; if modeling engine,
         * also publish all other prototypes.
         */
        for (IPrototype p : KLAB.ENGINE.getFunctionPrototypes()) {
            if (KLAB.ENGINE instanceof IModelingEngine
                    || (p.getComponentId() != null
                            && KLAB.PMANAGER.getComponent(p.getComponentId()) != null)) {
                ret.getFunctions().add(KLAB.MFACTORY.adapt(p, Service.class));
            }
        }

        for (String s : KLAB.ENGINE.getResourceConfiguration().getSynchronizedProjectIds()) {
            IProject project = KLAB.PMANAGER.getProject(s);
            if (project != null
                    && KLAB.ENGINE.getResourceConfiguration()
                            .isAuthorized(project, user, request.getRemoteAddr())) {
                ret.getSynchronizedProjectUrns().add(ResourceFactory.getProjectUrn(project));
            }
        }

        if (KLAB.ENGINE instanceof IModelingEngine) {
            ret.setEngineUser(((User) ((IModelingEngine) KLAB.ENGINE).getUser()).getProfile());
            ret.setCommunicationChannel(((IModelingEngine) KLAB.ENGINE).getCommunicationChannel());
        }

        ret.setModelCount(ModelKbox.get().count());
        ret.setObservationCount(ObservationKbox.get().count());
        ret.setBootMode(KLAB.ENGINE instanceof IModelingEngine ? BootMode.MODELER.name()
                : BootMode.NODE.name());

        return ret;
    }

    /**
     * Simply check if the passed request comes from the same host we run on.
     * 
     * @param request
     * @return true if request is from local address
     */
    public static boolean isLocalIp(HttpServletRequest request) {
        try {
            return request.getRemoteAddr().startsWith("127.")
                    || request.getRemoteAddr().startsWith("localhost")
                    || IPUtils.isLocal(request.getRemoteAddr());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * @param headers
     * @param request
     * @return 0 if OK
     * @throws KlabAuthorizationException
     */
    @RequestMapping(value = API.SHUTDOWN, method = RequestMethod.GET)
    public int shutdown(@RequestHeader HttpHeaders headers, HttpServletRequest request)
            throws KlabAuthorizationException {

        if (!(isLocalIp(request) || isAdmin(headers))) {
            throw new KlabAuthorizationException("no authorization for shutdown");
        }

        KLAB.ENGINE.shutdown(2);

        return 0;
    }

    /**
     * Check a request's headers against the configured administrative key for
     * session-less access to administrative functions.
     * 
     * @param headers
     * @return true if the request contains the installed administrative key in
     *         the authentication header.
     */
    public static boolean isAdmin(HttpHeaders headers) {
        if (KLAB.ENGINE.getResourceConfiguration().getAdministrationKey() == null) {
            return false;
        }
        if (headers.get(API.AUTHENTICATION_HEADER) != null) {
            for (String h : headers.get(API.AUTHENTICATION_HEADER)) {
                if (h.equals(KLAB.ENGINE.getResourceConfiguration().getAdministrationKey())) {
                    return true;
                }
            }
        }
        return false;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy