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

org.opentripplanner.updater.vehicle_rental.datasources.GbfsFeedLoader Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
package org.opentripplanner.updater.vehicle_rental.datasources;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.entur.gbfs.v2_2.gbfs.GBFS;
import org.entur.gbfs.v2_2.gbfs.GBFSFeed;
import org.entur.gbfs.v2_2.gbfs.GBFSFeedName;
import org.entur.gbfs.v2_2.gbfs.GBFSFeeds;
import org.opentripplanner.util.HttpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

/**
 * Class for managing the state and loading of complete GBFS datasets, and updating them according to individual feed's
 * TTL rules.
 */
public class GbfsFeedLoader {
    private static final Logger LOG = LoggerFactory.getLogger(GbfsFeedLoader.class);

    private static final ObjectMapper objectMapper = new ObjectMapper();

    static { objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); }

    /** One updater per feed type(?)*/
    private final Map> feedUpdaters = new HashMap<>();

    private final Map httpHeaders;

    public GbfsFeedLoader(String url, Map httpHeaders, String languageCode) {
        this.httpHeaders = httpHeaders;
        URI uri;
        try {
            uri = new URI(url);
        } catch (URISyntaxException e) {
            throw new RuntimeException("Invalid url " + url);
        }

        if (!url.endsWith("gbfs.json")) {
            LOG.warn("GBFS autoconfiguration url {} does not end with gbfs.json. Make sure it follows the specification, if you get any errors using it.", url);
        }

        // Fetch autoconfiguration file
        GBFS data = fetchFeed(uri, httpHeaders, GBFS.class);
        if (data == null) {
            throw new RuntimeException("Could not fetch the feed auto-configuration file from " + uri);
        }

        // Pick first language if none defined
        GBFSFeeds feeds = languageCode == null
                ? data.getFeedsData().values().iterator().next()
                : data.getFeedsData().get(languageCode);
        if (feeds == null) {
            throw new RuntimeException("Language " + languageCode + " does not exist in feed " + uri);
        }

        // Create updater for each file
        for (GBFSFeed feed : feeds.getFeeds()) {
            GBFSFeedName feedName = feed.getName();
            if (feedUpdaters.containsKey(feedName)) {
                throw new RuntimeException(
                        "Feed contains duplicate url for feed " + feedName + ". " +
                        "Urls: " + feed.getUrl() + ", " + feedUpdaters.get(feedName).url
                );
            }

            // name is null, if the file is of unknown type, skip those
            if (feed.getName() != null) {
                feedUpdaters.put(feedName, new GBFSFeedUpdater<>(feed));
            }

        }
    }

    /**
     * Checks if any of the feeds should be updated base on the TTL and fetches. Returns true, if any feeds were updated.
     */
    public boolean update() {
        boolean didUpdate = false;

        for (GBFSFeedUpdater updater : feedUpdaters.values()) {
            if (updater.shouldUpdate()) {
                boolean success = updater.fetchData();
                if (!success) {
                    return false;
                }
                didUpdate = true;
            }
        }

        return didUpdate;
    }

    /**
     * Gets the most recent contents of the feed, which contains an object of type T.
     */
    public  T getFeed(Class feed) {
        GBFSFeedUpdater updater = feedUpdaters.get(GBFSFeedName.fromClass(feed));
        if (updater == null) { return null; }
        return feed.cast(updater.getData());
    }

    /* private static methods */

    private static  T fetchFeed(URI uri, Map httpHeaders, Class clazz) {
        try {
            InputStream is;

            String proto = uri.getScheme();
            if (proto.equals("http") || proto.equals("https")) {
                is = HttpUtils.getData(uri, httpHeaders);
            } else {
                // Local file probably, try standard java
                is = uri.toURL().openStream();
            }
            if (is == null) {
                LOG.warn("Failed to get data from url {}", uri);
                return null;
            }
            T data = objectMapper.readValue(is, clazz);
            is.close();
            return data;
        } catch (IllegalArgumentException e) {
            LOG.warn("Error parsing vehicle rental feed from " + uri, e);
            return null;
        } catch (JsonProcessingException e) {
            LOG.warn("Error parsing vehicle rental feed from (bad JSON of some sort)" + uri);
            LOG.warn(e.getMessage());
            return null;
        } catch (IOException e) {
            LOG.warn("Error reading vehicle rental feed from (connection error)" + uri);
            LOG.warn(e.getMessage());
            return null;
        }
    }

    /* private static classes */

    private class GBFSFeedUpdater {

        /** URL for the individual GBFS file */
        private final URI url;

        /** To which class should the file be deserialized to */
        private final Class implementingClass;

        private int nextUpdate;
        private T data;

        private GBFSFeedUpdater(GBFSFeed feed) {
            url = feed.getUrl();
            implementingClass = (Class) feed.getName().implementingClass();
        }

        private T getData() {
            return data;
        }

        private boolean fetchData() {
            T newData = GbfsFeedLoader.fetchFeed(url, httpHeaders, implementingClass);
            if (newData == null) {
                LOG.error("Invalid data for {}", url);
                nextUpdate = getCurrentTimeSeconds();
                return false;
            }
            data = newData;

            try {
                // Fetch lastUpdated and ttl from the resulting class. Due to type erasure we don't know the actual
                // class, and have to use introspection to get the method references, as they do not share a supertype.
                Integer lastUpdated = (Integer) implementingClass.getMethod("getLastUpdated").invoke(newData);
                Integer ttl = (Integer) implementingClass.getMethod("getTtl").invoke(newData);
                if (lastUpdated == null || ttl == null) {
                    nextUpdate = getCurrentTimeSeconds();
                } else {
                    nextUpdate = lastUpdated + ttl;
                }
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | ClassCastException e) {
                LOG.error("Invalid lastUpdated or ttl for {}", url);
                nextUpdate = getCurrentTimeSeconds();
            }
            return true;
        }

        private boolean shouldUpdate() {
            return getCurrentTimeSeconds() >= nextUpdate;
        }

        private int getCurrentTimeSeconds() {
            return (int) (System.currentTimeMillis() / 1000);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy