/* (c) 2015 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.internal; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.sql.DataSource; import org.geoserver.platform.resource.Paths; import org.geoserver.jdbcstore.internal.JDBCQueryHelper.*; import static org.geoserver.jdbcstore.internal.JDBCQueryHelper.*; /** * * Handles database access & ORM mapping of directory structure * * @author Kevin Smith, Boundless * @author Niels Charlier * * */ public class JDBCDirectoryStructure { private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(JDBCDirectoryStructure.class); protected final static String TABLE_RESOURCES = "resources"; protected final static Field<Integer> OID = new Field<Integer>("oid", "oid", TYPE_INT); protected final static Field<String> NAME = new Field<String>("name", "name", TYPE_STRING); protected final static Field<Integer> PARENT = new Field<Integer>("parent", "parent", TYPE_INT); protected final static Field<Timestamp> LAST_MODIFIED = new Field<Timestamp>("last_modified", "last_modified", TYPE_TIMESTAMP); protected final static Field<InputStream> CONTENT = new Field<InputStream>("content", "content", TYPE_BLOB); protected final static Field<Boolean> DIRECTORY = new Field<Boolean>("directory", "content IS NULL AS directory", TYPE_BOOLEAN); private JDBCResourceStoreProperties config; private JDBCQueryHelper helper; /** * Resource/Directory entry in the database. * */ public class Entry { private final List<String> path; public Entry(List<String> path) { this.path = new ArrayList<String>(path); } public Entry(List<String> parent, String child) { path = new ArrayList<String>(parent); path.add(child); } public Entry(String path) { this.path = Paths.names(path); } @SuppressWarnings("unchecked") protected <T> T getValue(Field<T> prop) { Map<String, Object> record = helper.selectQuery(TABLE_RESOURCES, new PathSelector(path), prop); return record == null ? null : (T) record.get(prop.getFieldName()); } public Integer getOid() { return getValue(OID); } public Entry getParent() { return path.isEmpty() ? null : new Entry(path.subList(0, path.size() - 1)); } public Boolean isDirectory() { return getValue(DIRECTORY); // null safe } public String getName() { return path.isEmpty() ? null : path.get(path.size() - 1); } public List<String> getPath() { return Collections.unmodifiableList(path); } public Timestamp getLastModified() { return getValue(LAST_MODIFIED); } public List<Entry> getChildren() { List<Entry> list = new ArrayList<Entry>(); Integer oid = getOid(); if (oid != null) { for (Map<String, Object> result : helper.multiSelectQuery(TABLE_RESOURCES, new FieldSelector<Integer>(PARENT, oid), NAME)) { list.add(new Entry(path, (String) result.get(NAME.getFieldName()))); } } return list; } public boolean delete() { Integer oid = getOid(); if (oid == null) { LOGGER.warning("Attempting to delete undefined entry " + toString()); return false; } if (!deleteChildren(oid)) { LOGGER.warning("Delete operation failed or incomplete for entry " + toString()); return false; } if (helper.deleteQuery(TABLE_RESOURCES, new FieldSelector<Integer>(OID, oid)) <= 0) { LOGGER.warning("Delete operation failed or incomplete for entry " + toString()); return false; } return true; } public boolean renameTo(Entry dest) { Integer oid = getOid(); if (oid == null) { LOGGER.warning("Attempted to rename undefined entry: " + toString()); return false; } Integer destParentOid = dest.getValue(PARENT); if (destParentOid != null) { if (config.isDeleteDestinationOnRename()) { if (!dest.delete()) { LOGGER.warning("Rename operation failed for entry " + toString() + ": unable to delete destination of rename operation."); return false; } } else { LOGGER.warning("Rename operation failed for entry " + toString() + ": destination of rename operation is defined."); return false; } } else { Entry destParent = dest.getParent(); try { destParent.createDirectory(); } catch (IllegalStateException e) { LOGGER.log(Level.WARNING, "Rename operation failed for entry " + toString() + ": could not create parent directory.", e); return false; } destParentOid = destParent.getOid(); } if (helper.updateQuery(TABLE_RESOURCES, new FieldSelector<Integer>(OID, oid), new Assignment<String>(NAME, dest.getName()), new Assignment<Integer>(PARENT, destParentOid)) <= 0) { LOGGER.warning("Unable to perform rename operation for entry " + toString()); return false; } ; return true; } public InputStream getContent() { InputStream is = helper.blobQuery(TABLE_RESOURCES, new PathSelector(path), CONTENT); if (is == null) { throw new IllegalStateException("Could not find content for entry " + toString()); } return is; } public void setContent(InputStream is) { if (helper.updateQuery(TABLE_RESOURCES, new PathSelector(path), new Assignment<InputStream>(CONTENT, is), new Assignment<Timestamp>( LAST_MODIFIED, new Timestamp(new java.util.Date().getTime()))) <= 0) { LOGGER.warning("Unable to write content to entry " + toString()); } } public void createDirectory() { int parentOid = 0; for (String name : path) { Map<String, Object> record = helper.selectQuery(TABLE_RESOURCES, new ChildSelector( parentOid, name), OID, DIRECTORY); if (record == null) { parentOid = helper.insertQuery(TABLE_RESOURCES, new Assignment<String>(NAME, name), new Assignment<Integer>(PARENT, parentOid)); } else { if (!(Boolean) record.get(DIRECTORY.getFieldName())) { throw new IllegalStateException("Could not create directory at " + toString() + ": one of its parents exists and is not a directory."); } parentOid = (Integer) record.get(OID.getFieldName()); } } } public boolean createResource() { Boolean dir = isDirectory(); if (dir != null) { if (dir) { throw new IllegalStateException("Could not create resource at " + toString() + ": already a directory."); } else { return false; } } Entry parent = getParent(); try { parent.createDirectory(); } catch (IllegalStateException e) { throw new IllegalStateException("Could not create resource at " + toString() + ": could not create parent directory.", e); } ByteArrayInputStream is = new ByteArrayInputStream(new byte[0]); Integer oid = helper.insertQuery(TABLE_RESOURCES, new Assignment<String>(NAME, getName()), new Assignment<Integer>(PARENT, parent.getOid()), new Assignment<InputStream>( CONTENT, is)); try { is.close(); } catch (IOException e) { LOGGER.warning("Failed to close stream: " + toString()); } if (oid == null) { throw new IllegalStateException("Did not get OID for new entry " + toString()); } return true; } public String toString() { StringBuilder buf = new StringBuilder(); for (int i = 0; i < path.size(); i++) { if (i > 0) { // no leading slash buf.append("/"); } buf.append(path.get(i)); } return buf.toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getStructure().hashCode(); result = prime * result + getPath().hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof Entry)) { return false; } Entry other = (Entry) obj; return getStructure().equals(other.getStructure()) && getPath().equals(other.getPath()); } protected JDBCDirectoryStructure getStructure() { return JDBCDirectoryStructure.this; } } public JDBCDirectoryStructure(DataSource ds, JDBCResourceStoreProperties config) { this.helper = new JDBCQueryHelper(ds); this.config = config; if (config.isInitDb()) { LOGGER.log(Level.INFO, "Initializing Resource Store Database."); helper.runScript(config.getInitScript()); config.setInitDb(false); try { config.save(); } catch (IOException e) { LOGGER.log(Level.WARNING, "Unable to save ResourceStore configuration", e); } } } public Entry createEntry(String path) { return new Entry(path); } public JDBCResourceStoreProperties getConfig() { return config; } // ------------------------------ private helper methods & classes private boolean deleteChildren(Integer oid) { // get ids of children List<Integer> children = new ArrayList<Integer>(); for (Map<String, Object> result : helper.multiSelectQuery(TABLE_RESOURCES, new FieldSelector<Integer>(PARENT, oid), OID)) { children.add((Integer) result.get(OID.getFieldName())); } if (children.size() > 0) { // recursively apply to children for (Integer child : children) { if (!deleteChildren(child)) { return false; } } // delete all children in one go if (helper.deleteQuery(TABLE_RESOURCES, new FieldSelector<Integer>(PARENT, oid)) < children .size()) { return false; } } return true; } private static class PathSelector implements Selector { private List<String> path; private int contextOid; public PathSelector(List<String> path) { this(0, path); } public PathSelector(int contextOid, List<String> path) { this.path = path; this.contextOid = contextOid; } private void oidQuery(QueryBuilder builder, int i) { assert (i >= 0); assert (i < path.size()); if (i > 0) { builder.append("SELECT oid FROM " + TABLE_RESOURCES + " WHERE parent=("); oidQuery(builder, i - 1); builder.append(") and name=? "); builder.addParameter(new Parameter<String>(TYPE_STRING, path.get(i))); } else { builder.append("SELECT oid FROM " + TABLE_RESOURCES + " WHERE parent=? and name=? "); builder.addParameter(new Parameter<Integer>(TYPE_INT, contextOid)); builder.addParameter(new Parameter<String>(TYPE_STRING, path.get(i))); } } @Override public QueryBuilder appendCondition(QueryBuilder qb) { if (path.size() > 0) { qb.append("oid = ("); oidQuery(qb, path.size() - 1); qb.append(")"); } else { qb.append("oid = ?"); qb.addParameter(new Parameter<Integer>(TYPE_INT, contextOid)); } return qb; } } private static class ChildSelector implements Selector { private String name; private int parentOid; public ChildSelector(int parentOid, String name) { this.name = name; this.parentOid = parentOid; } @Override public QueryBuilder appendCondition(QueryBuilder qb) { qb.append("parent=? and name=? "); qb.addParameter(new Parameter<Integer>(TYPE_INT, parentOid)); qb.addParameter(new Parameter<String>(TYPE_STRING, name)); return qb; } } }