/* This program 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, either version 3 of
the License, or (props, at your option) 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
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.routing.impl;
import java.io.File;
import java.util.Collection;
import java.util.HashSet;
import java.util.Timer;
import java.util.TimerTask;
import javax.annotation.PostConstruct;
import lombok.Setter;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Graph.LoadLevel;
import org.opentripplanner.routing.services.GraphService;
import org.opentripplanner.routing.services.StreetVertexIndexFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
/**
* An implementation of the file-based GraphServiceFileImpl which auto-configure itself by scanning
* the root resource directory.
*/
@Scope("singleton")
public class GraphServiceAutoDiscoverImpl implements GraphService {
private static final Logger LOG = LoggerFactory.getLogger(GraphServiceAutoDiscoverImpl.class);
private GraphServiceFileImpl decorated = new GraphServiceFileImpl();
/** Last timestamp upper bound when we auto-scanned resources. */
private long lastAutoScan = 0L;
/** The autoscan period in seconds */
@Setter
private int autoScanPeriodSec = 60;
/**
* The delay before loading a new graph, in seconds. We load a graph if it has been modified at
* least this amount of time in the past. This in order to give some time for non-atomic graph
* copy.
*/
@Setter
private int loadDelaySec = 10;
/**
* @param indexFactory
*/
public void setIndexFactory(StreetVertexIndexFactory indexFactory) {
decorated.setIndexFactory(indexFactory);
}
/**
* @param defaultRouterId
*/
public void setDefaultRouterId(String defaultRouterId) {
decorated.setDefaultRouterId(defaultRouterId);
}
/**
* Sets a base path for graph loading from the filesystem. Serialized graph files will be
* retrieved from sub-directories immediately below this directory. The routerId of a graph is
* the same as the name of its sub-directory. This does the same thing as setResource, except
* the parameter is interpreted as a file path.
*/
public void setPath(String path) {
decorated.setBasePath(path);
}
@Override
public Graph getGraph() {
return decorated.getGraph();
}
@Override
public Graph getGraph(String routerId) {
return decorated.getGraph(routerId);
}
@Override
public void setLoadLevel(LoadLevel level) {
decorated.setLoadLevel(level);
}
@Override
public boolean reloadGraphs(boolean preEvict) {
return decorated.reloadGraphs(preEvict);
}
@Override
public Collection<String> getRouterIds() {
return decorated.getRouterIds();
}
@Override
public boolean registerGraph(String routerId, boolean preEvict) {
// Invalid in auto-discovery mode
return false;
}
@Override
public boolean registerGraph(String routerId, Graph graph) {
// Invalid in auto-discovery mode
return false;
}
@Override
public boolean evictGraph(String routerId) {
// Invalid in auto-discovery mode
return false;
}
@Override
public int evictAll() {
// Invalid in auto-discovery mode
return 0;
}
/**
* Based on the autoRegister list, automatically register all routerIds for which we can find a
* graph file in a subdirectory of the resourceBase path. Also register and load the graph for
* the defaultRouterId and warn if no routerIds are registered.
*/
@PostConstruct
// PostConstruct means run on startup after all injection has occurred
private void startup() {
/* Run the first one syncronously as other initialization methods may need a default router. */
autoDiscoverGraphs();
/*
* Starting with JDK7 we should use a directory change listener callback on baseResource
* instead.
*/
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
autoDiscoverGraphs();
}
}, autoScanPeriodSec * 1000L, autoScanPeriodSec * 1000L);
}
private synchronized void autoDiscoverGraphs() {
LOG.debug("Auto discovering graphs under {}", decorated.getBasePath());
Collection<String> graphOnDisk = new HashSet<String>();
Collection<String> graphToLoad = new HashSet<String>();
// Only reload graph modified more than 1 mn ago.
long validEndTime = System.currentTimeMillis() - loadDelaySec * 1000;
File baseFile = new File(decorated.getBasePath());
// First check for a root graph
File rootGraphFile = new File(baseFile, GraphServiceFileImpl.GRAPH_FILENAME);
if (rootGraphFile.exists() && rootGraphFile.canRead()) {
graphOnDisk.add("");
// lastModified can change, so test must be atomic here.
long lastModified = rootGraphFile.lastModified();
if (lastModified > lastAutoScan && lastModified <= validEndTime) {
LOG.debug("Graph to (re)load: {}, lastModified={}", rootGraphFile, lastModified);
graphToLoad.add("");
}
}
// Then graph in sub-directories
for (String sub : baseFile.list()) {
File subFile = new File(baseFile, sub);
if (subFile.isDirectory()) {
File graphFile = new File(subFile, GraphServiceFileImpl.GRAPH_FILENAME);
if (graphFile.exists() && graphFile.canRead()) {
graphOnDisk.add(sub);
long lastModified = graphFile.lastModified();
if (lastModified > lastAutoScan && lastModified <= validEndTime) {
LOG.debug("Graph to (re)load: {}, lastModified={}", graphFile,
lastModified);
graphToLoad.add(sub);
}
}
}
}
lastAutoScan = validEndTime;
StringBuffer onDiskSb = new StringBuffer();
for (String routerId : graphOnDisk)
onDiskSb.append("[").append(routerId).append("]");
StringBuffer toLoadSb = new StringBuffer();
for (String routerId : graphToLoad)
toLoadSb.append("[").append(routerId).append("]");
LOG.debug("Found routers: {} - Must reload: {}", onDiskSb.toString(), toLoadSb.toString());
for (String routerId : graphToLoad) {
/*
* Do not set preEvict, because: 1) during loading of a new graph we want to keep one
* available; and 2) if the loading of a new graph fails we also want to keep the old
* one.
*/
decorated.registerGraph(routerId, false);
}
for (String routerId : getRouterIds()) {
// Evict graph removed from disk.
if (!graphOnDisk.contains(routerId)) {
LOG.warn("Auto-evicting routerId '{}', not present on disk anymore.", routerId);
decorated.evictGraph(routerId);
}
}
/*
* If the defaultRouterId is not present, print a warning and set it to some default.
*/
if (!getRouterIds().contains(decorated.getDefaultRouterId())) {
LOG.warn("Default routerId '{}' not available!", decorated.getDefaultRouterId());
if (!getRouterIds().isEmpty()) {
// Let's see which one we want to take by default
String defRouterId = null;
if (getRouterIds().contains("")) {
// If we have a root graph, this should be a good default
defRouterId = "";
LOG.info("Setting default routerId to root graph ''");
} else {
// Otherwise take first one present
defRouterId = getRouterIds().iterator().next();
if (getRouterIds().size() > 1)
LOG.warn("Setting default routerId to arbitrary one '{}'", defRouterId);
else
LOG.info("Setting default routerId to '{}'", defRouterId);
}
decorated.setDefaultRouterId(defRouterId);
}
}
if (this.getRouterIds().isEmpty()) {
LOG.warn("No graphs have been loaded/registered. "
+ "You must place one or more graphs before routing.");
}
}
}