/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008, Open Source Geospatial Foundation (OSGeo) * (C) 2010, Geomatys * * This library 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; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. */ package org.geotoolkit.data.shapefile.lock; import java.io.*; import java.net.*; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.*; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import org.geotoolkit.data.shapefile.ShapefileFeatureStoreFactory; import static org.geotoolkit.data.shapefile.ShapefileFeatureStoreFactory.LOGGER; import static org.geotoolkit.data.shapefile.lock.ShpFileType.QIX; import static org.geotoolkit.data.shapefile.lock.ShpFileType.SHP; import org.geotoolkit.index.quadtree.QuadTree; import org.geotoolkit.index.quadtree.StoreException; import org.geotoolkit.index.quadtree.fs.FileSystemIndexStore; import org.apache.sis.util.collection.WeakHashSet; import org.geotoolkit.nio.IOUtilities; /** * The collection of all the files that are the shapefile and its metadata and * indices. * * <p> * This class has methods for performing actions on the files. Currently mainly * for obtaining read and write channels and streams. But in the future a move * method may be introduced. * </p> * * <p> * Note: The method that require locks (such as getInputStream()) will * automatically acquire locks and the javadocs should document how to release * the lock. Therefore the methods {@link #acquireRead(ShpFileType, FileReader)} * and {@link #acquireWrite(ShpFileType, FileWriter)} * </p> * * @author jesse * @author Johann Sorel (Geomatys) * @module */ public final class ShpFiles { /** * The uris for each type of file that is associated with the shapefile. The * key is the type of file */ private final Map<ShpFileType, URI> uris = new EnumMap<>(ShpFileType.class); /** * A read/write lock, so that we can have concurrent readers */ private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final WeakHashSet<AccessManager> managers = new WeakHashSet<>(AccessManager.class); private final boolean loadQuadTree; /** * Searches for all the files and adds then to the map of files. * * @param path any one of the shapefile files * @throws IllegalArgumentException if the shapefile associated with file is not found */ public ShpFiles(final Object path) throws IllegalArgumentException { this(path,false); } /** * Searches for all the files and adds then to the map of files. * * @param path any one of the shapefile files * @param loadQix If we should use quad-tree index on input files. */ public ShpFiles(final Object path, final boolean loadQix) throws IllegalArgumentException { URI uri = null; if(path instanceof String){ try { uri = URI.create(path.toString()); } catch (IllegalArgumentException e) { uri = Paths.get(path.toString()).toUri(); } }else if(path instanceof Path){ uri = ((Path) path).toUri(); }else if(path instanceof URI){ uri = (URI) path; }else if(path instanceof File){ uri = ((File) path).toURI(); }else if(path instanceof URL){ try { uri = ((URL) path).toURI(); } catch (URISyntaxException ex) { throw new IllegalArgumentException( "URL object can not be converted to a valid URI",ex); } }else{ throw new IllegalArgumentException( "Path object can not be converted to a valid URI : " +path); } loadQuadTree = loadQix; final String base = baseName(uri); if (base == null) { throw new IllegalArgumentException(uri.getPath() + " is not one of the files types that is known to be associated with a shapefile"); } //final String urlString = url.toExternalForm(); final String uriString = uri.toString(); final char lastChar = uriString.charAt(uriString.length()-1); final boolean upperCase = Character.isUpperCase(lastChar); //retrive all file uris associated with this shapefile for(final ShpFileType type : ShpFileType.values()) { final String extensionWithPeriod; if(upperCase){ extensionWithPeriod = type.extensionWithPeriod.toUpperCase(); }else{ extensionWithPeriod = type.extensionWithPeriod.toLowerCase(); } // TODO find a better way final URI newURI = URI.create(base + extensionWithPeriod); uris.put(type, newURI); } // if the files are local check each file to see if it exists // if not then search for a file of the same name but try all combinations of the // different cases that the extension can be made up of. // IE Shp, SHP, Shp, ShP etc... if( isWritable() ){ for (final Entry<ShpFileType, URI> entry : uris.entrySet()) { if( !exists(entry.getKey()) ){ URI value = entry.getValue(); final Path candidate = findExistingFile(Paths.get(value)); if(candidate!=null){ uris.put(entry.getKey(), candidate.toUri()); } } } } } public AccessManager createLocker(){ final AccessManager locker = new AccessManager(this); managers.add(locker); return locker; } void acquireReadLock(){ readWriteLock.readLock().lock(); } void releaseReadLock(){ readWriteLock.readLock().unlock(); } void acquireWriteLock(){ readWriteLock.writeLock().lock(); } void releaseWriteLock(){ readWriteLock.writeLock().unlock(); } /** * @return the URLs (in string form) of all the files for the shapefile datastore. */ public Map<ShpFileType, String> getFileNames() { final Map<ShpFileType, String> result = new EnumMap<>(ShpFileType.class); for (final Entry<ShpFileType, URI> entry : uris.entrySet()) { result.put(entry.getKey(), entry.getValue().toString()); } return result; } /** * Returns the string form of the url that identifies the file indicated by * the type parameter or null if it is known that the file does not exist. * * <p> * Note: a URL should NOT be constructed from the string instead the URL * should be obtained through calling one of the aquireLock methods. * * @param type * indicates the type of file the caller is interested in. * * @return the string form of the url that identifies the file indicated by * the type parameter or null if it is known that the file does not * exist. */ public String get(final ShpFileType type) { return uris.get(type).toString(); } /** * Acquire a Path for read only purposes. It is recommended that get*Stream * or get*Channel methods are used when reading or writing to the file is * desired. * * * @see #getInputStream(ShpFileType, FileReader) * @see #getReadChannel(ShpFileType, FileReader) * @see #getWriteChannel(org.geotoolkit.data.shapefile.ShpFileType, * org.geotoolkit.data.shapefile.FileWriter) * * @param type the type of the file desired. * @return the Path type requested * * @throws IllegalArgumentException If the given shapefile type has no valid * URI associated. * @throws FileSystemNotFoundException The file system, identified by the * URI of the input shapefile type, does not exist and cannot be created * automatically, or the provider identified by the URI's scheme component * is not installed * @throws SecurityException if a security manager is installed and it * denies an unspecified permission to access the file system */ public Path getPath(final ShpFileType type) { return Paths.get(getURI(type)); } /** * Acquire a URL for read only purposes. It is recommended that get*Stream or * get*Channel methods are used when reading or writing to the file is * desired. * * * @see #getInputStream(ShpFileType, FileReader) * @see #getReadChannel(ShpFileType, FileReader) * @see #getWriteChannel(org.geotoolkit.data.shapefile.ShpFileType, org.geotoolkit.data.shapefile.FileWriter) * * @param type * the type of the file desired. * @return the URL to the file of the type requested */ public URI getURI(final ShpFileType type) { return uris.get(type); } /** * Determine if the location of this shapefile is local or remote. * * @return true if local, false if remote */ public boolean isWritable() { try { Path path = getPath(SHP); if (!Files.exists(path)) { /* If the file to test does not exist, we must ensure that it's * first existing parent is writable, i.e we can create new data * in it. */ if (path.getFileSystem().isReadOnly()) { return false; } path = path.getParent(); while (!(Files.exists(path) || path.equals(path.getRoot()))) { path = path.getParent(); } } return Files.isWritable(path); } catch (Exception e) { // if not a path, maybe it's a simple URL access for download. In all // case, we'll need NIO API for writing purpose. LOGGER.log(Level.FINE, "SHP URI cannot be converted to NIO Path.", e); return false; } } /** * Delete all the shapefile files. * * @throws IOException e If we failed deleting any of the files, or if the * store is read-only. */ public void delete() throws IOException { if (!isWritable()) throw new IOException("Read-only datastore"); acquireWriteLock(); try { for (URI uri : uris.values()) { Files.deleteIfExists(Paths.get(uri)); } } finally { releaseWriteLock(); } } /** * Opens a input stream for the indicated file. * * @param type * the type of file to open the stream to. * @return an input stream * * @throws IOException * if a problem occurred opening the stream. */ public InputStream getInputStream(final ShpFileType type) throws IOException { return IOUtilities.open(getURI(type)); } /** * Opens a output stream for the indicated file. * * @param type * the type of file to open the stream to. * @return an output stream * * @throws IOException * if a problem occurred opening the stream. */ public OutputStream getOutputStream(final ShpFileType type) throws IOException { return IOUtilities.openWrite(getURI(type), StandardOpenOption.CREATE, StandardOpenOption.WRITE); } /** * Obtain a ReadableByteChannel from the given URL. If the url protocol is * file, a FileChannel will be returned. Otherwise a generic channel will be * obtained from the uris input stream. * <p> * A read lock is obtained when this method is called and released when the * channel is closed. * </p> * * @param type the type of file to open the channel to. * @return A read-only channel on the requested file. * @throws java.io.IOException If we cannot open a channel on input data. * */ public ReadableByteChannel getReadChannel(final ShpFileType type) throws IOException { final URI uri = getURI(type); return getReadChannel(uri); } public ReadableByteChannel getReadChannel(final URI uri) throws IOException { return Files.newByteChannel(Paths.get(uri), StandardOpenOption.READ); } /** * Obtain a WritableByteChannel from the given URL. If the url protocol is * file, a FileChannel will be returned. Currently, this method will return * a generic channel for remote uris, however both shape and dbf writing can * only occur with a local FileChannel channel. * * <p> * A write lock is obtained when this method is called and released when the * channel is closed. * </p> * * * @param type * the type of file to open the stream to. * * @return a WritableByteChannel for the provided file type * * @throws IOException * if there is an error opening the stream */ public WritableByteChannel getWriteChannel(final ShpFileType type) throws IOException { return getWriteChannel(getURI(type)); } public WritableByteChannel getWriteChannel(final URI uri) throws IOException { return Files.newByteChannel(Paths.get(uri)); } public String getTypeName() { final String path = SHP.toBase(uris.get(SHP)); final int slash = Math.max(0, path.lastIndexOf('/') + 1); return path.substring(slash, path.length()); } /** * Returns true if the file exists. * Throws an exception if the file is not local. * * @param fileType the type of file to check existance for. * @return true if the file exists. * @throws IllegalArgumentException if the files are not local. */ public boolean exists(final ShpFileType fileType) throws IllegalArgumentException { return exists(uris.get(fileType)); } public boolean exists(final URI uri) throws IllegalArgumentException { if (uri == null) { return false; } try { return Files.exists(Paths.get(uri)); } catch (Exception e) { // If we were not able to transform input into path, maybe it's an http URL. try { uri.toURL().openConnection().connect(); return true; } catch (IOException e1) { e1.addSuppressed(e); LOGGER.log(Level.FINE, "Cannot connect to ".concat(uri.toString()), e1); return false; } } } //////////////////////////////////////////////////////////////////////////// /////////////// utils methods ////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// private static String baseName(final URI obj) { for(final ShpFileType type : ShpFileType.values()) { String base = type.toBase( (URI)obj ); if (base != null) { return base; } } return null; } /** * Search for a file of the same name but try all combinations of the * different cases that the extension can be made up of. * exemple : Shp, SHP, Shp, ShP etc... */ private static Path findExistingFile(final Path file) { final Path directory = file.getParent(); if( directory==null || !Files.exists(directory) ) { // doesn't exist return null; } List<Path> matchingPaths = new ArrayList<>(); final String baseFileName = file.getFileName().toString(); try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory)) { for (Path path : directoryStream) { final String filename = IOUtilities.filename(path); if (baseFileName.equalsIgnoreCase(filename)) { matchingPaths.add(path); } } } catch (IOException e) { ShapefileFeatureStoreFactory.LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); } if(!matchingPaths.isEmpty()){ return matchingPaths.get(0); } return null; } //////////////////////////////////////////////////////////////////////////// // Indexes files : store only one of each for all readers ////////////////// //////////////////////////////////////////////////////////////////////////// private FileSystemIndexStore qixStore = null; private QuadTree quadTree = null; public synchronized void unloadIndexes(){ if(quadTree != null){ try { quadTree.close(); } catch (StoreException ex) { LOGGER.log(Level.WARNING, "Failed to close quad tree.", ex); quadTree = null; } } } public synchronized QuadTree getQIX() throws StoreException{ if(quadTree == null){ if (!isWritable()) { return null; } final URI treeURI = getURI(QIX); try { final Path treePath = IOUtilities.toPath(treeURI); if (!Files.exists(treePath) || (Files.size(treePath) == 0)) { return null; } if(qixStore == null){ qixStore = new FileSystemIndexStore(treePath); } if(loadQuadTree){ //we store the quad tree for reuse quadTree = qixStore.load(); quadTree.loadAll(); return quadTree; }else{ return qixStore.load(); } } catch (IOException ex) { LOGGER.log(Level.WARNING, "Failed to get quad tree.", ex); return null; } } return quadTree; } @Override public String toString() { final StringBuilder builder = new StringBuilder("Shapefile URIs :"); for (final Map.Entry<ShpFileType, URI> entry : uris.entrySet()) { builder.append(System.lineSeparator()).append(entry.getKey().name()).append(" -> ").append(entry.getValue()); } return builder.toString(); } }