/* (c) 2015 - 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.jdbcstore; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Logger; import javax.sql.DataSource; import org.apache.commons.io.output.ProxyOutputStream; import org.apache.commons.lang.ArrayUtils; import org.geoserver.jdbcstore.cache.ResourceCache; import org.geoserver.jdbcstore.internal.JDBCDirectoryStructure; import org.geoserver.jdbcstore.internal.JDBCResourceStoreProperties; import org.geoserver.platform.resource.LockProvider; import org.geoserver.platform.resource.NullLockProvider; import org.geoserver.platform.resource.Paths; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.ResourceListener; import org.geoserver.platform.resource.ResourceNotification; import org.geoserver.platform.resource.ResourceStore; import org.geoserver.platform.resource.ResourceNotificationDispatcher; import org.geoserver.platform.resource.Resources; import org.geoserver.platform.resource.SimpleResourceNotificationDispatcher; /** * Implementation of ResourceStore backed by a JDBC DirectoryStructure. * * @author Kevin Smith, Boundless * @author Niels Charlier * */ public class JDBCResourceStore implements ResourceStore { private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(JDBCResourceStore.class); /** LockProvider used to secure resources for exclusive access */ protected LockProvider lockProvider = new NullLockProvider(); protected ResourceNotificationDispatcher resourceNotificationDispatcher = new SimpleResourceNotificationDispatcher(); protected JDBCDirectoryStructure dir; protected ResourceCache cache; protected ResourceStore oldResourceStore; public void setCache(ResourceCache cache) { this.cache = cache; } LockProvider getLockProvider() { return lockProvider; } /** * Configure LockProvider used during {@link Resource#out()}. * * @param lockProvider LockProvider used for Resource#out() */ public void setLockProvider(LockProvider lockProvider) { this.lockProvider = lockProvider; } /** * Configure ResourceWatcher * * @param resourceWatcher */ public void setResourceNotificationDispatcher(ResourceNotificationDispatcher resourceWatcher) { this.resourceNotificationDispatcher = resourceWatcher; } public JDBCResourceStore(JDBCDirectoryStructure dir) { this.dir = dir; } public JDBCResourceStore(DataSource ds, JDBCResourceStoreProperties config) { this(new JDBCDirectoryStructure(ds, config)); } public JDBCResourceStore(DataSource ds, JDBCResourceStoreProperties config, ResourceStore oldResourceStore) { this(ds, config); this.oldResourceStore = oldResourceStore; if (config.isImport()) { if (oldResourceStore != null) { try { Resource root = oldResourceStore.get(""); for (Resource child : root.list()) { if (!ArrayUtils.contains(config.getIgnoreDirs(), child.name())) { Resources.copy(child, get(child.name())); } } config.setImport(false); config.save(); } catch (IOException e) { throw new IllegalStateException(e); } } else { LOGGER.warning("Cannot import resources: no old resource store available."); } } } @Override public Resource get(String path) { List<String> pathNames = Paths.names(path); if (oldResourceStore != null && pathNames.size() > 0 && ArrayUtils.contains(dir.getConfig().getIgnoreDirs(), pathNames.get(0))) { return oldResourceStore.get(path); } return new JDBCResource(dir.createEntry(path)); } @Override public boolean remove(String path) { return dir.createEntry(path).delete(); } @Override public boolean move(String path, String target) { return dir.createEntry(path).renameTo(dir.createEntry(target)); } @Override public String toString() { return "JDBCResourceStore"; } /** * Direct implementation of Resource. * */ protected class JDBCResource implements Resource { private final JDBCDirectoryStructure.Entry entry; public JDBCResource(JDBCDirectoryStructure.Entry entry) { assert(entry != null); this.entry = entry; } @Override public String path() { return entry.toString(); } @Override public String name() { return entry.getName(); } private InputStream getIStream() { return entry.getContent(); } @Override public InputStream in() { final Lock lock = lock(); try { entry.createResource(); return getIStream(); } finally { lock.release(); } } @Override public OutputStream out() { List<ResourceNotification.Event> events = SimpleResourceNotificationDispatcher.createEvents(this, ResourceNotification.Kind.ENTRY_CREATE); if (entry.createResource()) { resourceNotificationDispatcher.changed(new ResourceNotification(path(), ResourceNotification.Kind.ENTRY_CREATE, System.currentTimeMillis(), events)); } try { return new CachingOutputStreamWrapper(File.createTempFile("out.", entry.getName())); } catch (IOException ex) { throw new IllegalStateException(ex); } } @Override public File file() { if (getType() == Type.DIRECTORY) { throw new IllegalStateException("Directory (not a file)"); } final Lock lock = lock(); try { return cache.cache(this, false); } catch(IOException ex) { throw new IllegalStateException(ex); } finally { lock.release(); } } @Override public File dir() { if (getType() == Type.RESOURCE) { throw new IllegalStateException("File (not a directory)"); } final Lock lock = lock(); try { return cache.cache(this, true); } catch(IOException ex) { throw new IllegalStateException(ex); } finally { lock.release(); } } @Override public long lastmodified() { Timestamp ts = entry.getLastModified(); return ts == null ? 0L : ts.getTime(); } @Override public Resource parent() { return new JDBCResource(entry.getParent()); } @Override public Resource get(String resourcePath) { return JDBCResourceStore.this.get(path() + "/" + resourcePath); } @Override public List<Resource> list() { if (getType() != Type.DIRECTORY) { return Collections.EMPTY_LIST; } List<Resource> list = new ArrayList<Resource>(); for (JDBCDirectoryStructure.Entry child : entry.getChildren()) { list.add(new JDBCResource(child)); } return list; } @Override public Type getType() { Boolean isDir = entry.isDirectory(); return isDir == null ? Type.UNDEFINED : isDir ? Type.DIRECTORY : Type.RESOURCE ; } @Override public int hashCode() { return entry.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof JDBCResource)) { return false; } JDBCResource other = (JDBCResource) obj; return entry.equals(other.entry); } @Override public boolean delete() { List<Lock> locks = new ArrayList<Lock>(); lockRecursively(locks); try { List<ResourceNotification.Event> events = SimpleResourceNotificationDispatcher.createEvents(this, ResourceNotification.Kind.ENTRY_DELETE); if (entry.delete()) { resourceNotificationDispatcher.changed(new ResourceNotification(path(), ResourceNotification.Kind.ENTRY_DELETE, System.currentTimeMillis(), events)); return true; } else { return false; } } finally { for (Lock lock : locks) { lock.release(); } } } private void lockRecursively(List<Lock> locks) { for (JDBCDirectoryStructure.Entry child : entry.getChildren()) { new JDBCResource(child).lockRecursively(locks); } locks.add(lock()); } @Override public boolean renameTo(Resource dest) { List<ResourceNotification.Event> eventsDelete = SimpleResourceNotificationDispatcher.createEvents(this, ResourceNotification.Kind.ENTRY_DELETE); List<ResourceNotification.Event> eventsRename = SimpleResourceNotificationDispatcher.createRenameEvents(this, dest); boolean result; if (dest instanceof JDBCResource) { result = entry.renameTo(((JDBCResource) dest).entry); } else { result = Resources.renameByCopy(this, dest); } if (result) { resourceNotificationDispatcher.changed(new ResourceNotification(path(), ResourceNotification.Kind.ENTRY_DELETE, System.currentTimeMillis(), eventsDelete)); resourceNotificationDispatcher.changed(new ResourceNotification(path(), eventsRename.get(0).getKind(), System.currentTimeMillis(), eventsRename)); } return result; } @Override public Lock lock() { return lockProvider.acquire(entry.toString()); } @Override public void addListener(ResourceListener listener) { resourceNotificationDispatcher.addListener(path(), listener); } @Override public void removeListener(ResourceListener listener) { resourceNotificationDispatcher.removeListener(path(), listener); } /** * Use temporary file for writing data to resource. * */ protected class CachingOutputStreamWrapper extends ProxyOutputStream { File tempFile; public CachingOutputStreamWrapper(File tempFile) throws IOException { super(new FileOutputStream(tempFile)); this.tempFile = tempFile; } @Override public void close() throws IOException { final Lock lock = lock(); try { List<ResourceNotification.Event> events = SimpleResourceNotificationDispatcher.createEvents(JDBCResource.this, ResourceNotification.Kind.ENTRY_MODIFY); entry.setContent(new FileInputStream(tempFile)); resourceNotificationDispatcher.changed(new ResourceNotification(path(), ResourceNotification.Kind.ENTRY_MODIFY, System.currentTimeMillis(), events)); } finally { lock.release(); } } } } @Override public ResourceNotificationDispatcher getResourceNotificationDispatcher() { return resourceNotificationDispatcher; } }