DriverFactory.java

/* 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.esigate;

import static org.apache.commons.lang3.StringUtils.defaultIfBlank;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.esigate.Driver.DriverBuilder;
import org.esigate.http.IncomingRequest;
import org.esigate.impl.IndexedInstances;
import org.esigate.impl.UriMapping;
import org.esigate.util.UriUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Factory class used to configure and retrieve {@linkplain Driver} INSTANCIES.
 * 
 * @author Stanislav Bernatskyi
 * @author Francois-Xavier Bonnet
 * @author Nicolas Richeton
 */
public final class DriverFactory {

    /**
     * The provider context, composed of driver and remote relative url.
     */
    static final class MatchedRequest {
        private final Driver driver;
        private final String relativeUri;

        private MatchedRequest(Driver driver, String relativeUri) {
            this.driver = driver;
            this.relativeUri = relativeUri;
        }

        String getRelativeUri() {
            return relativeUri;
        }

        Driver getDriver() {
            return driver;
        }

    }

    /**
     * System property used to specify of esigate configuration, outside of the classpath.
     */
    public static final String PROP_CONF_LOCATION = "esigate.config";
    private static IndexedInstances instances = new IndexedInstances(new HashMap<String, Driver>());
    private static final String DEFAULT_INSTANCE_NAME = "default";
    private static final Logger LOG = LoggerFactory.getLogger(DriverFactory.class);

    static {
        String version =
                defaultIfBlank(DriverFactory.class.getPackage().getSpecificationVersion(), "development version");
        String rev = defaultIfBlank(DriverFactory.class.getPackage().getImplementationVersion(), "unknown");
        LOG.info("Starting esigate {} rev. {}", version, rev);
    }

    private DriverFactory() {
        // Do not instantiate
    }

    /**
     * Returns the collection of all {@link Driver} instances.
     * 
     * @return All configured driver
     */
    public static Collection<Driver> getInstances() {
        DriverFactory.ensureConfigured();
        return instances.getInstances().values();

    }

    /**
     * Loads all instances according to default configuration file.
     */
    public static void configure() {
        InputStream inputStream = null;

        try {
            URL configUrl = getConfigUrl();

            if (configUrl == null) {
                throw new ConfigurationException("esigate.properties configuration file "
                        + "was not found in the classpath");
            }

            inputStream = configUrl.openStream();

            Properties merged = new Properties();
            if (inputStream != null) {
                Properties props = new Properties();
                props.load(inputStream);
                merged.putAll(props);
            }

            configure(merged);
        } catch (IOException e) {
            throw new ConfigurationException("Error loading configuration", e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }

            } catch (IOException e) {
                throw new ConfigurationException("failed to close stream", e);
            }
        }
    }

    /**
     * Loads all instances according to the properties parameter.
     * 
     * @param props
     *            properties to use for configuration
     */
    public static void configure(Properties props) {
        Properties defaultProperties = new Properties();

        HashMap<String, Properties> driversProps = new HashMap<>();
        for (Enumeration<?> enumeration = props.propertyNames(); enumeration.hasMoreElements();) {
            String propertyName = (String) enumeration.nextElement();
            String value = props.getProperty(propertyName);
            int idx = propertyName.lastIndexOf('.');
            if (idx < 0) {
                defaultProperties.put(propertyName, value);
            } else {
                String prefix = propertyName.substring(0, idx);
                String name = propertyName.substring(idx + 1);
                Properties driverProperties = driversProps.get(prefix);
                if (driverProperties == null) {
                    driverProperties = new Properties();
                    driversProps.put(prefix, driverProperties);
                }
                driverProperties.put(name, value);
            }
        }

        // Merge with default properties
        Map<String, Driver> newInstances = new HashMap<>();
        for (Entry<String, Properties> entry : driversProps.entrySet()) {
            String name = entry.getKey();
            Properties properties = new Properties();
            properties.putAll(defaultProperties);
            properties.putAll(entry.getValue());
            newInstances.put(name, createDriver(name, properties));
        }
        if (newInstances.get(DEFAULT_INSTANCE_NAME) == null
                && Parameters.REMOTE_URL_BASE.getValue(defaultProperties) != null) {

            newInstances.put(DEFAULT_INSTANCE_NAME, createDriver(DEFAULT_INSTANCE_NAME, defaultProperties));
        }

        instances = new IndexedInstances(newInstances);
    }

    private static Driver createDriver(String name, Properties properties) {
        DriverBuilder builder = Driver.builder().setName(name).setProperties(properties);
        return builder.build();
    }

    /**
     * Registers new {@linkplain Driver} under provided name with specified properties.
     * 
     * @param name
     *            the name of the instance
     * @param props
     *            the {@link Properties} for the instance
     */
    public static void configure(String name, Properties props) {
        put(name, createDriver(name, props));
    }

    /**
     * Retrieves the default instance of this class that is configured according to the properties file
     * (driver.properties).
     * 
     * @param instanceName
     *            The name of the instance (corresponding to the prefix in the driver.properties file)
     * 
     * @return the named instance
     */
    public static Driver getInstance(String instanceName) {
        if (instanceName == null) {
            instanceName = DEFAULT_INSTANCE_NAME;
        }
        if (instances.getInstances().isEmpty()) {
            throw new ConfigurationException("Driver has not been configured and driver.properties file was not found");
        }
        Driver instance = instances.getInstances().get(instanceName);
        if (instance == null) {
            throw new ConfigurationException("No configuration properties found for factory : " + instanceName);
        }
        return instance;
    }

    /**
     * Selects the Driver instance for this request based on the mappings declared in the configuration.
     * 
     * @param request
     *            the incoming request
     * 
     * @return a {@link MatchedRequest} containing the {@link Driver} instance and the relative URI
     * 
     * @throws HttpErrorPage
     *             if no instance was found for this request
     */
    static MatchedRequest selectProvider(IncomingRequest request) throws HttpErrorPage {
        URI requestURI = UriUtils.createURI(request.getRequestLine().getUri());
        String host = UriUtils.extractHost(requestURI).toHostString();
        Header hostHeader = request.getFirstHeader(HttpHeaders.HOST);
        if (hostHeader != null) {
            host = hostHeader.getValue();
        }
        String scheme = requestURI.getScheme();
        String relativeUri = requestURI.getPath();
        String contextPath = request.getContextPath();
        if (!StringUtils.isEmpty(contextPath) && relativeUri.startsWith(contextPath)) {
            relativeUri = relativeUri.substring(contextPath.length());
        }

        Driver driver = null;
        UriMapping uriMapping = null;
        for (UriMapping mapping : instances.getUrimappings().keySet()) {
            if (mapping.matches(scheme, host, relativeUri)) {
                driver = getInstance(instances.getUrimappings().get(mapping));
                uriMapping = mapping;
                break;
            }
        }
        if (driver == null) {
            throw new HttpErrorPage(HttpStatus.SC_NOT_FOUND, "Not found", "No mapping defined for this URI.");
        }

        if (driver.getConfiguration().isStripMappingPath()) {
            relativeUri = DriverFactory.stripMappingPath(relativeUri, uriMapping);
        }

        MatchedRequest context = new MatchedRequest(driver, relativeUri);
        LOG.debug("Selected {} for scheme:{} host:{} relUrl:{}", driver, scheme, host, relativeUri);
        return context;
    }

    /**
     * Retrieves the default instance of this class that is configured according to the properties file
     * (driver.properties).
     * 
     * @return the default instance
     */
    public static Driver getInstance() {
        return getInstance(DEFAULT_INSTANCE_NAME);
    }

    /**
     * Add/replace instance in current instance map. Work on a copy of the current map and replace it atomically.
     * 
     * @param instanceName
     *            The name of the provider
     * @param instance
     *            The instance
     */
    public static void put(String instanceName, Driver instance) {
        // Copy current instances
        Map<String, Driver> newInstances = new HashMap<>();
        synchronized (instances) {
            Set<String> keys = instances.getInstances().keySet();

            for (String key : keys) {
                newInstances.put(key, instances.getInstances().get(key));
            }
        }

        // Add new instance
        newInstances.put(instanceName, instance);

        instances = new IndexedInstances(newInstances);
    }

    /**
     * Ensure configuration has been loaded at least once. Helps to prevent delay on first call because of
     * initialization.
     */
    public static void ensureConfigured() {
        if (instances.getInstances().isEmpty()) {
            // Load default settings
            configure();
        }
    }

    /**
     * Returns the {@link URL} of the configuration file.
     * 
     * @return The URL of the configuration file.
     */
    public static URL getConfigUrl() {
        URL configUrl = null;
        // Load from environment
        String envPath = System.getProperty(PROP_CONF_LOCATION);
        if (envPath != null) {
            try {
                LOG.info("Scanning configuration {}", envPath);
                configUrl = new File(envPath).toURI().toURL();
            } catch (MalformedURLException e) {
                LOG.error("Can't read file {} (from -D" + PROP_CONF_LOCATION + ")", envPath, e);
            }
        }

        if (configUrl == null) {
            LOG.info("Scanning configuration {}", "/esigate.properties");
            configUrl = DriverFactory.class.getResource("/esigate.properties");
        }

        // For backward compatibility
        if (configUrl == null) {
            LOG.info("Scanning configuration /{}/{}", DriverFactory.class.getPackage().getName().replace(".", "/"),
                    "driver.properties");
            configUrl = DriverFactory.class.getResource("driver.properties");
        }
        if (configUrl == null) {
            LOG.info("Scanning configuration {}", "/net/webassembletool/driver.properties");
            configUrl = DriverFactory.class.getResource("/net/webassembletool/driver.properties");
        }
        return configUrl;
    }

    /**
     * Get the relative url without the mapping url.
     * <p/>
     * Uses the url and remove the mapping path.
     * 
     * @param url
     *            incoming relative url
     * @return the url, relative to the driver remote url.
     */
    static String stripMappingPath(String url, UriMapping mapping) {
        String relativeUrl = url;
        // Uri mapping
        String mappingPath;
        if (mapping == null) {
            mappingPath = null;
        } else {
            mappingPath = mapping.getPath();
        }

        // Remove mapping path
        if (mappingPath != null && url.startsWith(mappingPath)) {
            relativeUrl = relativeUrl.substring(mappingPath.length());
        }
        return relativeUrl;
    }

    /**
     * Selects the Driver instance for this request based on the mappings declared in the configuration and executes it
     * against the selected {@link Driver} instance.
     * 
     * @param incomingRequest
     *            the incoming request
     * 
     * @return a {@link MatchedRequest} containing the {@link Driver} instance and the relative URI
     * 
     * @throws HttpErrorPage
     *             if no instance was found for this request or if an error occurs
     * @throws IOException
     *             if an error occurs
     */
    public static CloseableHttpResponse proxy(IncomingRequest incomingRequest) throws IOException, HttpErrorPage {
        MatchedRequest matchedRequest = selectProvider(incomingRequest);
        return matchedRequest.getDriver().proxy(matchedRequest.getRelativeUri(), incomingRequest);
    }

}