/* Copyright 2013 The jeo project. All rights reserved. * * 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 io.jeo.data; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import io.jeo.vector.VectorDriver; import io.jeo.vector.Schema; import io.jeo.util.Util; import io.jeo.vector.VectorDataset; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static io.jeo.util.Util.isEmptyOrNull; /** * Driver utility class. * <p> * This class is used to abstract away driver specifics when interacting with data. For example it * can be used to create a data object from a file. * <pre><code> * Driver.open(new File("states.json")); * </code></pre> * </p> * * @author Justin Deoliveira, OpenGeo */ public class Drivers { /** logger */ static final Logger LOG = LoggerFactory.getLogger(Drivers.class); /** driver registry */ public static final DriverRegistry REGISTRY = new ServiceLoaderDriverRegistry(); /** * Lists all registered drivers. */ public static Iterable<Driver<?>> list() { return list(REGISTRY); } /** * Lists all registered drivers from the specified registry. */ public static Iterable<Driver<?>> list(DriverRegistry registry) { return list(Driver.class, registry); } /** * Lists all registered drivers that extend from the specified class. */ public static Iterable<Driver<?>> list(final Class<?> filter) { return list(filter, REGISTRY); } /** * Lists all registered drivers from the specified registry that extend from the specified class. */ public static Iterable<Driver<?>> list(final Class<?> filter, DriverRegistry registry) { final Iterator<? extends Driver<?>> it = registry.list(); return new Iterable<Driver<?>>() { @Override public Iterator<Driver<?>> iterator() { return new Iterator<Driver<?>>() { Driver<?> next; @Override public boolean hasNext() { while(next == null && it.hasNext()) { next = it.next(); if (filter == null || !filter.isInstance(next)) { next = null; } } return next != null; } @Override public Driver<?> next() { try { return next; } finally { next = null; } } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Looks up a driver by name. * * @see Drivers#find(String, DriverRegistry) */ public static Driver<?> find(String name) { return find(name, REGISTRY); } /** * Looks up a driver by name from the specified registry. * <p> * This method does a case-insensitive comparison. * </p> * * @param name The driver name. * * @return The matching driver, or <code>null</code> if no match was found. * * @see Driver#name() */ public static Driver<?> find(String name, DriverRegistry registry) { for (Driver<?> d : list(registry)) { if (name.equalsIgnoreCase(d.name()) || d.aliases().contains(name)) { return d; } } return null; } /** * Looks up a driver from a data uri. * * @see Drivers#find(URI, DriverRegistry)}. */ public static Driver<?> find(URI uri) { return find(uri, REGISTRY); } /** * Looks up a driver from a data uri. * * @param uri A uri defining a data source, as described by * {@link #open(URI, Class, DriverRegistry)}. * * @return The matching driver, or <code>null</code> if no match was found. */ public static <T> Driver<T> find(URI uri, DriverRegistry registry) { uri = convertFileURI(uri); String scheme = uri.getScheme(); if (scheme == null) { LOG.debug("URI must have scheme: " + uri); return null; //throw new IllegalArgumentException("URI must have a scheme"); } Driver<T> d = (Driver<T>) find(scheme, registry); if (d == null) { LOG.debug("No matching driver for " + scheme); return null; } return d; } /** * Opens a connection to data specified by a file. * * @see Drivers#open(File, Class, DriverRegistry) */ public static Object open(File file) throws IOException { return open(file, REGISTRY); } /** * Opens a connection to data specified by a file using the specified driver registry. * * @see Drivers#open(File, Class, DriverRegistry) */ public static Object open(File file, DriverRegistry registry) throws IOException { return open(file, Object.class, registry); } /** * Opens a connection to data specified by a file expecting an object of the specified class. * * @see Drivers#open(File, Class, DriverRegistry) */ public static <T> T open(File file, Class<T> clazz) throws IOException { return open(file, clazz, REGISTRY); } /** * Opens a connection to data specified by a file. * <p> * The optional <tt>class</tt> parameter is used to filter the candidate driver set. For * example to constrain to workspace drivers. * <pre><code> * Workspace ws = Drivers.open(..., Workspace.class); * </code></pre> * </p> * @param file The file to open. * @param clazz Class used to filter registered drivers, may be <code>null</code>. * * @return The data object, or <code>null</code> if no suitable driver could be found for the * specified file. * * @throws IOException Any connection errors, such as a file system error or * database connection failure. */ public static <T> T open(File file, Class<T> clazz, DriverRegistry registry) throws IOException { return open(file.toURI(), clazz, registry); } /** * Opens a connection to data described by the specified options. * * @param opts Connection options. * * @return The data object, or <code>null</code> if no suitable driver could be found for the * specified options. * @see #open(Map, Class) */ public static <T> T open(Map<?, Object> opts) throws IOException { return (T) open(opts, Object.class, REGISTRY); } /** * Opens a connection to data described by the specified options. * <p> * The optional <tt>class</tt> parameter is used to filter the candidate driver set. For * example to constrain to workspace drivers. * <pre><code> * Workspace ws = Drivers.open(..., Workspace.class); * </code></pre> * </p> * @param opts Connection options. * @param clazz Class used to filter registered drivers, may be <code>null</code>. * * @return The data object, or <code>null</code> if no suitable driver could be found for the * specified options. * * @throws IOException Any connection errors, such as a file system error or * database connection failure. */ public static <T> T open(Map<?, Object> opts, Class<T> clazz) throws IOException { return open(opts, clazz, REGISTRY); } public static <T> T open(Map<?, Object> opts, Class<T> clazz, DriverRegistry registry) throws IOException { return open(opts, clazz, list(registry)); } /** * Opens a connection to data described by the specified uri. * * @see Drivers#open(URI, Class, DriverRegistry) */ public static Object open(URI uri) throws IOException { return open(uri, REGISTRY); } /** * Opens a connection to data described by the specified uri with the specified driver * registry. * * @see Drivers#open(URI, Class, DriverRegistry) */ public static Object open(URI uri, DriverRegistry registry) throws IOException { return open(uri, Object.class, registry); } /** * Opens a connection to data of the specified type described by the specified uri. * * @see Drivers#open(URI, Class, DriverRegistry) */ public static <T> T open(URI uri, Class<T> clazz) throws IOException { return open(uri, clazz, REGISTRY); } /** * Opens a connection to data described by the specified uri. * <p> * The <tt>uri</tt> can take one of two forms. The first is a file uri: * <pre> * file:<path> * </pre> * For example <tt>file:/Users/jdeolive/foo.json</tt> specifies a connection to a GeoJSON file. * </p> * <p> * The second form uses the uri components to describe different aspect of the connection: * <pre> * [<driver>://][<primary-option>][?<secondary-options>]*][#<dataset>] * </pre> * Where: * <ul> * <li><tt>driver</tt> is the driver name or alias * <li><tt>primary-option</tt> is the "main" option for that driver, for example in the case * of file based drivers this would be the file path, in the case of database based drivers * this would be the database name. * <li><tt>secondary-options</tt> are additional driver options * <li><tt>dataset</tt> is the name of a dataset within a workspace * </ul> * For example <tt>pg://jeo?host=locahost&port=5432&user=bob#states</tt> specifies a * connection to a PostGIS table named "states", in a database named "jeo", as user "bob", to * a server running on localhost, port 5432. * </p> * <p> * The optional <tt>class</tt> parameter is used to filter the candidate driver set. For * example to constrain to workspace drivers. * <pre><code> * Workspace ws = Drivers.open(..., Workspace.class); * </code></pre> * </p> * @param uri uri specifying connection options. * @param clazz Class used to filter registered drivers, may be <code>null</code>. * @param registry the non-null registry to search for drivers * * @return The data object, or <code>null</code> if no suitable driver could be found for the * specified options. * * @throws IOException Any connection errors, such as a file system error or * database connection failure. */ public static <T> T open(URI uri, Class<T> clazz, DriverRegistry registry) throws IOException { uri = convertFileURI(uri); Driver<?> d = find(uri, registry); if (d == null) { return null; } Map<String,Object> opts = parseURI(uri, d); Object data = d.open(opts); if (data instanceof Workspace && uri.getFragment() != null) { data = ((Workspace)data).get(uri.getFragment()); } if (clazz == Workspace.class && data instanceof Dataset) { data = new SingleWorkspace((Dataset) data); } return clazz.cast(data); } public static <T extends VectorDataset> T create(Schema schema, URI uri, Class<T> clazz) throws IOException { return create(schema, uri, clazz, REGISTRY); } public static <T extends VectorDataset> T create(Schema schema, URI uri, Class<T> clazz, DriverRegistry registry) throws IOException { uri = convertFileURI(uri); Driver<T> d = find(uri, registry); if (d == null) { throw new IllegalArgumentException("No driver for " + uri); } if (!(d instanceof VectorDriver)) { throw new IllegalArgumentException(d.name() + " not a vector driver"); } VectorDriver<?> vd = (VectorDriver<?>) d; Map<String,Object> opts = parseURI(uri, d); if (!vd.canCreate(opts, null)) { throw new IllegalArgumentException(d.name() + " driver can't open " + opts); } Object obj = vd.create(opts, schema); if (obj instanceof VectorDataset) { return (T) obj; } else if (obj instanceof Workspace) { Workspace ws = (Workspace) obj; return (T) ws.get(schema.name()); } throw new IllegalArgumentException(d.name() + " driver returned " + obj); } /** * Parses a jeo data uri into a map of connection parameters. */ public static Map<String,Object> parseURI(URI uri, Driver<?> d) { uri = convertFileURI(uri); Map<String,Object> opts = new HashMap<String, Object>(); // parse host / path String first = uri.getHost() != null ? uri.getHost() : uri.getPath(); if (!isEmptyOrNull(first)) { //use the first key if (!d.keys().isEmpty()) { opts.put(d.keys().get(0).name(), first); } } // parse query string if (!isEmptyOrNull(uri.getQuery())) { String[] kvps = uri.getQuery().split("&"); for (String kvp : kvps) { String[] kv = kvp.split("="); if (kv.length != 2) { throw new IllegalArgumentException("Illegal key value pair: " + kvp); } opts.put(kv[0], kv[1]); } } return opts; } static <T> T open(Map<?, Object> opts, Class<T> clazz, Iterable<Driver<?>> it) throws IOException { for (Driver<?> drv : it) { if (clazz != null && !clazz.isAssignableFrom(drv.type())) { continue; } if (drv.canOpen(opts, null)) { Object data = drv.open(opts); if (data != null) { return (T) data; } } } return null; } static URI convertFileURI(URI uri) { if (uri.getScheme() == null) { // treat as a file uri uri = new File(uri.toString()).toURI(); } if ("file".equalsIgnoreCase(uri.getScheme())) { //hack for files, turn file extension String ext = Util.extension(uri.getPath()); if (ext != null) { try { uri = new URI(String.format(Locale.ROOT,"%s://?file=%s%s", ext, uri.getPath(), uri.getFragment() != null ? "#"+uri.getFragment() : "")); } catch (URISyntaxException e) { } } } return uri; } }