/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ package org.fcrepo.server.storage.lowlevel; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Enumeration; import java.util.Map; import java.util.NoSuchElementException; import org.fcrepo.server.errors.LowlevelStorageException; import org.fcrepo.server.errors.LowlevelStorageInconsistencyException; import org.fcrepo.server.errors.ObjectNotInLowlevelStorageException; import org.fcrepo.server.storage.ConnectionPool; import org.fcrepo.server.utilities.SQLUtility; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Bill Niebel */ public class DBPathRegistry extends PathRegistry { private static final Logger logger = LoggerFactory.getLogger(DBPathRegistry.class); private static final String escapedBackslash = "\\\\"; //Java quotes will interpolate these as 2 backslashes private ConnectionPool connectionPool = null; private final String selectAllQuery; private final String selectByIdQuery; private final String deleteByIdQuery; private final boolean backslashIsEscape; public DBPathRegistry(Map<String, ?> configuration) throws LowlevelStorageException { super(configuration); connectionPool = (ConnectionPool) configuration.get("connectionPool"); backslashIsEscape = Boolean .valueOf((String) configuration .get("backslashIsEscape")).booleanValue(); selectAllQuery = "SELECT token FROM " + this.registryName; selectByIdQuery = "SELECT path FROM " + this.registryName + " WHERE token=?"; deleteByIdQuery = "DELETE FROM " + this.registryName + " WHERE " + this.registryName + ".token=?"; try { String dbSpec = "org/fcrepo/server/storage/resources/DBPathRegistry.dbspec"; InputStream specIn = this.getClass().getClassLoader() .getResourceAsStream(dbSpec); if (specIn == null) { throw new IOException("Cannot find required resource: " + dbSpec); } SQLUtility.createNonExistingTables(connectionPool, specIn); } catch (Exception e) { throw new LowlevelStorageException( true, "Error while attempting to check for and create non-existing table(s): " + e.getClass().getName() + ": " + e.getMessage(), e); } } /** * Checks to see whether a pid exists in the registry. * Makes no audits of the number of registrations or * the paths registered. */ @Override public boolean exists(String pid) throws LowlevelStorageException { Connection connection = null; PreparedStatement statement = null; ResultSet rs = null; try { connection = connectionPool.getReadOnlyConnection(); statement = connection.prepareStatement(selectByIdQuery); statement.setString(1,pid); rs = statement.executeQuery(); return rs.next(); } catch (SQLException e1) { throw new LowlevelStorageException(true, "sql failure (get)", e1); } finally { try { if (rs != null) { rs.close(); } if (statement != null) { statement.close(); } if (connection != null) { connectionPool.free(connection); } } catch (Exception e2) { // purposely general to include uninstantiated statement, connection throw new LowlevelStorageException(true, "sql failure closing statement, connection, pool (get)", e2); } finally { rs = null; statement = null; connection = null; } } } @Override public String get(String pid) throws ObjectNotInLowlevelStorageException, LowlevelStorageInconsistencyException, LowlevelStorageException { String path = null; Connection connection = null; PreparedStatement statement = null; ResultSet rs = null; try { int paths = 0; connection = connectionPool.getReadOnlyConnection(); statement = connection.prepareStatement(selectByIdQuery); statement.setString(1,pid); rs = statement.executeQuery(); for (; rs.next(); paths++) { path = rs.getString(1); } if (paths == 0) { throw new ObjectNotInLowlevelStorageException("no path in db registry for [" + pid + "]"); } if (paths > 1) { throw new LowlevelStorageInconsistencyException("[" + pid + "] in db registry -multiple- times"); } if (path == null || path.length() == 0) { throw new LowlevelStorageInconsistencyException("[" + pid + "] has -null- path in db registry"); } } catch (SQLException e1) { throw new LowlevelStorageException(true, "sql failure (get)", e1); } finally { try { if (rs != null) { rs.close(); } if (statement != null) { statement.close(); } if (connection != null) { connectionPool.free(connection); } } catch (Exception e2) { // purposely general to include uninstantiated statement, connection throw new LowlevelStorageException(true, "sql failure closing statement, connection, pool (get)", e2); } finally { rs = null; statement = null; connection = null; } } return path; } private void ensureSingleUpdate(Statement statement) throws ObjectNotInLowlevelStorageException, LowlevelStorageInconsistencyException, LowlevelStorageException { try{ int updateCount = statement.getUpdateCount(); if (updateCount == 0) { throw new ObjectNotInLowlevelStorageException("Object not found in low-level storage: -no- rows updated in db registry"); } if (updateCount > 1) { throw new LowlevelStorageInconsistencyException("-multiple- rows updated in db registry"); } } catch (SQLException e1) { throw new LowlevelStorageException(true, "sql failurex (exec)", e1); } } private void executeUpdate(String sql, String pid) throws ObjectNotInLowlevelStorageException, LowlevelStorageInconsistencyException, LowlevelStorageException { Connection connection = null; PreparedStatement statement = null; try { connection = connectionPool.getReadWriteConnection(); statement = connection.prepareStatement(sql); if (pid != null){ statement.setString(1,pid); } if (statement.execute()) { throw new LowlevelStorageException(true, "sql returned query results for a nonquery"); } ensureSingleUpdate(statement); } catch (SQLException e1) { throw new LowlevelStorageException(true, "sql failurex (exec)", e1); } finally { try { if (statement != null) { statement.close(); } if (connection != null) { connectionPool.free(connection); } } catch (Exception e2) { // purposely general to include uninstantiated statement, connection throw new LowlevelStorageException(true, "sql failure closing statement, connection, pool (exec)", e2); } finally { statement = null; connection = null; } } } @Override public void put(String pid, String path) throws ObjectNotInLowlevelStorageException, LowlevelStorageInconsistencyException, LowlevelStorageException { if (backslashIsEscape) { StringBuffer buffer = new StringBuffer(); /* * Escape each backspace so that DB will correctly record a single * backspace, instead of incorrectly escaping the following * character. */ for (int i = 0; i < path.length(); i++) { char s = path.charAt(i); buffer.append(s == '\\' ? escapedBackslash : s); } path = buffer.toString(); } Connection conn = null; try { conn = connectionPool.getReadWriteConnection(); SQLUtility.replaceInto(conn, getRegistryName(), new String[] { "token", "path"}, new String[] {pid, path}, "token"); } catch (SQLException e1) { throw new ObjectNotInLowlevelStorageException("put into db registry failed for [" + pid + "]", e1); } finally { if (conn != null) { connectionPool.free(conn); conn = null; } } } @Override public void remove(String pid) throws ObjectNotInLowlevelStorageException, LowlevelStorageInconsistencyException, LowlevelStorageException { try { executeUpdate(deleteByIdQuery, pid); } catch (ObjectNotInLowlevelStorageException e1) { throw new ObjectNotInLowlevelStorageException("[" + pid + "] not in db registry to delete", e1); } catch (LowlevelStorageInconsistencyException e2) { throw new LowlevelStorageInconsistencyException("[" + pid + "] deleted from db registry -multiple- times", e2); } } @Override public void rebuild() throws LowlevelStorageException { int report = FULL_REPORT; try { executeUpdate("DELETE FROM " + getRegistryName(), null); } catch (ObjectNotInLowlevelStorageException e1) { } catch (LowlevelStorageInconsistencyException e2) { } try { logger.info("begin rebuilding registry from files"); traverseFiles(storeBases, REBUILD, false, report); // continues, ignoring bad files logger.info("end rebuilding registry from files (ending normally)"); } catch (Exception e) { if (report != NO_REPORT) { logger.error("ending rebuild unsuccessfully", e); } throw new LowlevelStorageException(true, "ending rebuild unsuccessfully", e); //<<==== } } @Override public void auditFiles() throws LowlevelStorageException { logger.info("begin audit: files-against-registry"); traverseFiles(storeBases, AUDIT_FILES, false, FULL_REPORT); logger.info("end audit: files-against-registry (ending normally)"); } @Override public Enumeration<String> keys() throws LowlevelStorageException, LowlevelStorageInconsistencyException { File tempFile = null; PrintWriter writer = null; ResultSet rs = null; Connection connection = null; Statement statement = null; try { tempFile = File.createTempFile("fedora-keys", ".tmp"); writer = new PrintWriter(new OutputStreamWriter( new FileOutputStream(tempFile))); connection = connectionPool.getReadOnlyConnection(); statement = connection.createStatement(); rs = statement.executeQuery(selectAllQuery); while (rs.next()) { String key = rs.getString(1); if (null == key || 0 == key.length()) { connectionPool.free(connection); connection = null; throw new LowlevelStorageInconsistencyException( "Null token found in " + getRegistryName()); } writer.println(key); } writer.close(); return new KeyEnumeration(tempFile); } catch (Exception e) { throw new LowlevelStorageException(true, "Unexpected error", e); } finally { try { if (rs != null) { rs.close(); } if (statement != null) { statement.close(); } if (connection != null) { connectionPool.free(connection); } } catch (Exception e) { throw new LowlevelStorageException(true, "Unexpected error", e); } finally { if (writer != null) { writer.close(); writer = null; } rs = null; statement = null; connection = null; } } } /** * Iterates over each non-empty line in a temporary file. * When iteration is complete, or garbage collection occurs, the * file will be deleted. */ private class KeyEnumeration implements Enumeration<String> { private final File file; private final BufferedReader reader; private boolean closed; private String nextKey; public KeyEnumeration(File file) throws FileNotFoundException { this.file = file; this.reader = new BufferedReader(new InputStreamReader(new FileInputStream(file))); setNextKey(); } private void setNextKey() { try { nextKey = reader.readLine(); if (nextKey == null) { close(); } else if (nextKey.length() == 0) { setNextKey(); } } catch (IOException e) { throw new Error(e); } } @Override public boolean hasMoreElements() { return nextKey != null; } @Override public String nextElement() { if (nextKey != null) { try { return nextKey; } finally { setNextKey(); } } else { throw new NoSuchElementException(); } } @Override protected void finalize() { if (!closed) { close(); } } private void close() { try { reader.close(); file.delete(); } catch (IOException e) { throw new Error(e); } finally { closed = true; } } } }