/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2011, 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.FileNotFoundException; import java.io.IOException; import java.io.StringWriter; import java.net.URI; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.sis.io.TableAppender; import org.geotoolkit.data.dbf.DbaseFileReader; import org.geotoolkit.data.shapefile.fix.IndexedFidReader; import org.geotoolkit.data.shapefile.fix.IndexedFidWriter; import org.geotoolkit.data.shapefile.indexed.RecordNumberTracker; import org.geotoolkit.data.shapefile.shp.ShapefileReader; import org.geotoolkit.data.shapefile.shx.ShxReader; import org.geotoolkit.data.dbf.Closeable; import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.logging.Logging; /** * Manage reader and writer creation with proper read/write locks. * * @author Johann Sorel (Geomatys) * @module */ public final class AccessManager { private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.data.shapefile.lock"); private static class AccessEntry{ final ShpFileType type; final URI uri; final Closeable holder; public AccessEntry(ShpFileType type, URI uri, Closeable holder) { this.type = type; this.uri = uri; this.holder = holder; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(type.name()) .append("\t").append(!holder.isClosed()) .append("\t").append(uri) .append("\t").append(holder); return sb.toString(); } } private final ShpFiles files; private final List<AccessEntry> readEntries = new ArrayList<>(); private final List<AccessEntry> writeEntries = new ArrayList<>(); private final List<StorageFile> tempFiles = new ArrayList<>(); /** * Can only be created by a shpFiles object. * @param files */ AccessManager(final ShpFiles files){ this.files = files; } private void getReadLock(){ files.acquireReadLock(); } private void getWriteLock(){ files.acquireWriteLock(); } private void releaseReadLock(){ files.releaseReadLock(); } private void releaseWriteLock(){ files.releaseWriteLock(); } public DbaseFileReader getDBFReader(final boolean memoryMapped, final Charset set) throws IOException{ final URI uri = files.getURI(ShpFileType.DBF); if (uri == null) { return null; } if (files.isWritable() && !files.exists(ShpFileType.DBF)) { return null; } final ReadableByteChannel rbc = toClosingChannel(files.getReadChannel(uri),false); final DbaseFileReader reader = new DbaseFileReader(rbc, memoryMapped, set); readEntries.add(new AccessEntry(ShpFileType.DBF, uri, reader)); return reader; } public ShapefileReader getSHPReader(final boolean strict, final boolean memoryMapped, final boolean read3D, final double[] resample) throws IOException, DataStoreException{ final URI shpUrl = files.getURI(ShpFileType.SHP); final ReadableByteChannel shpChannel = toClosingChannel(files.getReadChannel(shpUrl),false); final URI shxUrl = files.getURI(ShpFileType.SHX); final ReadableByteChannel shxChannel; if (shxUrl == null || (files.isWritable() && !files.exists(shxUrl)) ) { //shx does not exist shxChannel = null; }else{ shxChannel = toClosingChannel(files.getReadChannel(shxUrl),false); } final ShapefileReader shpReader = new ShapefileReader( shpChannel,shxChannel,strict,memoryMapped,read3D,resample); readEntries.add(new AccessEntry(ShpFileType.SHP, shpUrl, shpReader)); readEntries.add(new AccessEntry(ShpFileType.SHX, shxUrl, shpReader)); return shpReader; } public ShxReader getSHXReader(final boolean memoryMapped) throws IOException { final URI shxUrl = files.getURI(ShpFileType.SHX); if (shxUrl == null) { return null; } if (files.isWritable() && !files.exists(shxUrl)) { return null; } final ReadableByteChannel shxChannel = toClosingChannel(files.getReadChannel(shxUrl),false); final ShxReader reader = new ShxReader(shxChannel, memoryMapped); readEntries.add(new AccessEntry(ShpFileType.SHX, shxUrl, reader)); return reader; } public IndexedFidReader getFIXReader(final RecordNumberTracker tracker) throws IOException{ final URI url = files.getURI(ShpFileType.FIX); final ReadableByteChannel rbc = toClosingChannel(files.getReadChannel(url),false); final IndexedFidReader reader = new IndexedFidReader(url,rbc, tracker); readEntries.add(new AccessEntry(ShpFileType.FIX, url, reader)); return reader; } public IndexedFidWriter getFIXWriter(final StorageFile storage) throws IOException{ if (!files.isWritable()) { throw new IllegalArgumentException( "Currently only local files are supported for writing"); } final URI url = files.getURI(ShpFileType.FIX); ReadableByteChannel rbc = null; try { rbc = toClosingChannel(files.getReadChannel(url),true); } catch (FileNotFoundException | NoSuchFileException e) { rbc = storage.getWriteChannel(); } final IndexedFidWriter writer = new IndexedFidWriter( url,rbc,storage.getWriteChannel()); writeEntries.add(new AccessEntry(ShpFileType.FIX, url, writer)); return writer; } /** * Obtains a Storage file for the type indicated. An id is provided so that * the same file can be obtained at a later time with just the id * * @param type the type of file to create and return * * @return StorageFile * @throws IOException if temporary files cannot be created */ public StorageFile getStorageFile(final ShpFileType type) throws IOException { String baseName = files.getTypeName(); if (baseName.length() < 3) { // min prefix length for createTempFile baseName = baseName + "___".substring(0, 3 - baseName.length()); } final Path tmp = Files.createTempFile(baseName, type.extensionWithPeriod); final StorageFile tempFile = new StorageFile(files, tmp, type); tempFiles.add(tempFile); return tempFile; } /** * Close all readers and writers. */ public void disposeReaderAndWriters(){ for(final AccessEntry entry : writeEntries){ try { entry.holder.close(); } catch (IOException ex) { LOGGER.log(Level.WARNING, "Failed to close writer : "+entry.holder, ex); } } writeEntries.clear(); for(final AccessEntry entry : readEntries){ try { entry.holder.close(); } catch (IOException ex) { LOGGER.log(Level.WARNING, "Failed to close reader : "+entry.holder, ex); } } readEntries.clear(); } public synchronized void replaceStorageFiles() throws IOException{ replaceStorageFiles(null); } /** * Aquiere a write lock and replace all storage files. * At this step all readers and writers must have been closed. */ public synchronized void replaceStorageFiles(Runnable postReplaceRunnable) throws IOException{ if(!allRWClosed()){ throw new IOException("Can not replace files while readers or writers are still open :\n"+this.toString()); } getWriteLock(); try{ final StorageFile[] files = tempFiles.toArray(new StorageFile[tempFiles.size()]); StorageFile.replaceOriginals(files); }finally{ tempFiles.clear(); //whatever happens we release the lock releaseWriteLock(); } if(postReplaceRunnable!=null){ postReplaceRunnable.run(); } } private boolean allRWClosed(){ boolean cleanState = true; for(final AccessEntry entry : readEntries){ cleanState &= entry.holder.isClosed(); } for(final AccessEntry entry : writeEntries){ cleanState &= entry.holder.isClosed(); } return cleanState; } /** * Will close all created readers and writers and release any lock. */ public void dispose(){ if(!tempFiles.isEmpty()){ LOGGER.log(Level.WARNING, "Disposing manager with temporary files remaining."); } for(final AccessEntry entry : writeEntries){ try { entry.holder.close(); } catch (IOException ex) { LOGGER.log(Level.WARNING, "Failed to close writer : "+entry.holder, ex); } } for(final AccessEntry entry : readEntries){ try { entry.holder.close(); } catch (IOException ex) { LOGGER.log(Level.WARNING, "Failed to close reader : "+entry.holder, ex); } } } @Override public String toString() { final StringBuilder sb = new StringBuilder(); try{ final StringWriter writer = new StringWriter(); final TableAppender tb = new TableAppender(writer); tb.appendHorizontalSeparator(); tb.append("type\topen\tpath\tholder\n"); tb.appendHorizontalSeparator(); tb.append("Reading\n"); for(final AccessEntry entry : readEntries){ tb.append(entry.toString()); tb.append('\n'); } tb.appendHorizontalSeparator(); tb.append("Writing\n"); for(final AccessEntry entry : writeEntries){ tb.append(entry.toString()); tb.append('\n'); } tb.appendHorizontalSeparator(); tb.flush(); sb.append(writer.getBuffer().toString()).append("\n"); }catch(IOException ex){ //will not happen } return sb.toString(); } @Override protected void finalize() throws Throwable { if(!allRWClosed()){ throw new IOException("Access Manager has not been closed in proper state, readers or writers are still open :\n"+this.toString()); } } private ReadableByteChannel toClosingChannel(final ReadableByteChannel channel, final boolean writing){ if(channel instanceof ClosingFileChannel || channel instanceof ClosingReadableByteChannel){ throw new RuntimeException("Wrapping an already auto closing channel."); } if(channel instanceof FileChannel){ return new ClosingFileChannel((FileChannel)channel, writing); }else{ return new ClosingReadableByteChannel(channel); } } private final class ClosingReadableByteChannel implements ReadableByteChannel{ private final ReadableByteChannel wrapped; private ClosingReadableByteChannel(final ReadableByteChannel wrapped) { this.wrapped = wrapped; getReadLock(); } @Override public int read(final ByteBuffer dst) throws IOException { return wrapped.read(dst); } @Override public boolean isOpen() { return wrapped.isOpen(); } @Override public void close() throws IOException { wrapped.close(); releaseReadLock(); } } private final class ClosingFileChannel extends FileChannel implements ReadableByteChannel{ private final FileChannel wrapped; private final boolean write; private boolean closed; private ClosingFileChannel(final FileChannel channel, final boolean write) { this.wrapped = channel; this.closed = false; this.write = write; if(!write){ getReadLock(); } } @Override public void force(final boolean metaData) throws IOException { wrapped.force(metaData); } @Override public FileLock lock(final long position, final long size, final boolean shared) throws IOException { return wrapped.lock(position, size, shared); } @Override public MappedByteBuffer map(final MapMode mode, final long position, final long size) throws IOException { return wrapped.map(mode, position, size); } @Override public long position() throws IOException { return wrapped.position(); } @Override public FileChannel position(final long newPosition) throws IOException { return wrapped.position(newPosition); } @Override public int read(final ByteBuffer dst, final long position) throws IOException { return wrapped.read(dst, position); } @Override public int read(final ByteBuffer dst) throws IOException { return wrapped.read(dst); } @Override public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { return wrapped.read(dsts, offset, length); } @Override public long size() throws IOException { return wrapped.size(); } @Override public long transferFrom(final ReadableByteChannel src, final long position, final long count) throws IOException { return wrapped.transferFrom(src, position, count); } @Override public long transferTo(final long position, final long count, final WritableByteChannel target) throws IOException { return wrapped.transferTo(position, count, target); } @Override public FileChannel truncate(final long size) throws IOException { return wrapped.truncate(size); } @Override public FileLock tryLock(final long position, final long size, final boolean shared) throws IOException { return wrapped.tryLock(position, size, shared); } @Override public int write(final ByteBuffer src, final long position) throws IOException { return wrapped.write(src, position); } @Override public int write(final ByteBuffer src) throws IOException { return wrapped.write(src); } @Override public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { return wrapped.write(srcs, offset, length); } @Override protected void implCloseChannel() throws IOException { try { wrapped.close(); } finally { if (!closed) { closed = true; if (!write) { releaseReadLock(); } } } } } }