package com.github.kilianB.matcher.persistent.database; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.SQLTimeoutException; import java.sql.Statement; import java.util.logging.Logger; import org.h2.tools.DeleteDbFiles; import com.github.kilianB.matcher.persistent.ConsecutiveMatcher; /** * A naive database based image matcher implementation. Images indexed by this * matcher will be added to the database and retrieved if an image match is * queried. * * <p> * This class is backed by the * <a href="http://www.h2database.com/html/main.html">h2 database engine</a> * * <p> * The image matcher supports chaining multiple hashing steps which will be * invoked in the order the algorithms were added. Once a hashing algorithm * fails to match a specific image the image is discarded pruning the search * tree quickly. * * <p> * Opposed to the {@link ConsecutiveMatcher} this matcher does not stores a * reference to the image data itself but just keeps track of the hash and the * url of the image file. Additionally if hashing algorithms are added after * images have been hashed the images will not be found without reindexing the * image in question.. * * <p> * Multiple database image matchers may use the same database in which case * hashes created by the same hashing algorithm will be used in both matchers. * * <pre> * {@code * * DatabaseImageMatcher matcher0, matcher1; * * matcher0.addHashingAlgorithm(new AverageHash(32),...,...) * matcher1.addHashingAlgorithm(new AverageHash(32),...,...) * * matcher0.addHashingAlgorithm(new AverageHash(24),...,...) * * matcher0.addImage(Image1) * } * </pre> * * Starting from this point matcher1 would also be able to match against * <i>Image1</i>. Be aware that this relationship isn't symmetric. Images added * by calling <i>matcher1.addImage(..)</i> method will be matched at the first * step in <i>matcher0</i> but fail to find a hash for * <code>AverageHash(24)</code> therefore discarding the image as a possible * match. * <p> * If this behaviour is not desired simply choose a different database for each * image matcher. * * <p> * 2 + n Tables are generated to save vales: * * <ol> * <li>ImageHasher(id,serialize): Allows to serialize an image matcher to the * database</li> * <li>HashingAlgos(id,keyLenght): Saves the bit resolution of each hashing * algorithm</li> * <li>... n a table for each hashing algorithm used in an image matcher</li> * </ol> * * <p> * For each and every match the hashes have to be read from the database. This * allows to persistently stores hashes but might not be as efficient as the * {@link ConsecutiveMatcher}. Optimizations may include to store 0 or 1 level * hashes (hashes created by the first invoked hashing algorithms at a memory * level and only retrieve the later hashes from the database. * * @author Kilian * @since 2.0.2 added * @since 3.0.0 extract h2 database image matcher into it's own class * */ public class H2DatabaseImageMatcher extends DatabaseImageMatcher { private static final long serialVersionUID = 5629316725655117532L; private static final Logger LOG = Logger.getLogger(H2DatabaseImageMatcher.class.getName()); private static final String CLASS_NOT_FOUND_ERROR = "In order to use the default database image " + "matcher please make sure to add a h2 dependency to the class path. (Last tested version: 1.4.197)."; /** * Get a database image matcher which previously got serialized by calling * {@link #serializeToDatabase(int)} on the object. * * @param subname the database file name. By default the file looks at the base * directory of the user. * <p> * <code>"jdbc:h2:~/" + subname</code> * * @param user the database user on whose behalf the connection is being * made. * @param password the user's password. May be empty * @param id the id supplied to the serializeDatabase call * @return the image matcher found in the database or null if not present * @throws SQLException if an error occurs while connecting to the database or * the h2 driver could not be found in the classpath * @since 3.0.0 */ public static H2DatabaseImageMatcher getFromDatabase(String subname, String user, String password, int id) throws SQLException { return (H2DatabaseImageMatcher) getFromDatabase(getConnection(subname, user, password), id); } /** * Attempts to establish a connection to the given database URL using the h2 * database driver. If the database does not yet exist an empty db will be * initialized. * * @param subname the database file name. By default the file looks at the base * directory of the user. * <p> * <code>"jdbc:h2:~/" + subname</code> * * @param user the database user on whose behalf the connection is being * made * @param password the user's password. May be empty * @exception SQLException if an error occurs while connecting to the database * or the h2 driver could not be found in the classpath * @throws SQLTimeoutException when the driver has determined that the timeout * value specified by the {@code setLoginTimeout} * method has been exceeded and has at least tried * to cancel the current database connection attempt * @since 3.0.0 */ public H2DatabaseImageMatcher(String subname, String user, String password) throws SQLException { this(getConnection(subname, user, password)); } /** * Attempts to establish a connection to the given database using the supplied * connection object. If the database does not yet exist an empty db will be * initialized. * * @param dbConnection the database connection * @throws SQLException if a database access error occurs * {@code null} * @throws SQLTimeoutException when the driver has determined that the * timeout value specified by the * {@code setLoginTimeout} method has been * exceeded and has at least tried to cancel * the current database connection attempt * @throws IllegalArgumentException if the supplied dbConnection is not an h2 * connection object */ public H2DatabaseImageMatcher(Connection dbConnection) throws SQLException { super(checkConnection(dbConnection)); } /** * Drop all data in the tables and delete the database files. This method * currently only supports h2 databases. After this method terminates * successfully all further method calls of this object will throw an * SQLException if applicable. * * <p> * Calling this method will close() all database connections. Therefore calling * close() on this object is not necessary. * * @throws SQLException if an SQL error occurs * @since 3.0.0 */ public void deleteDatabase() throws SQLException { try (Statement stm = conn.createStatement()) { String url = conn.getMetaData().getURL(); String needle = "jdbc:h2:"; if (url.startsWith(needle)) { int dbNameIndex = url.lastIndexOf("/"); String path = url.substring(needle.length(), dbNameIndex); String dbName = url.substring(dbNameIndex + 1); close(); DeleteDbFiles.execute(path, dbName, true); } else { String msg = "deleteDatabase currently not supported for non h2 drivers."; LOG.severe(msg); throw new UnsupportedOperationException(msg); } } } // Utility methods /** * Get a h2 database connection with the supplied settings. * * @param subname the database file name. By default the file looks at the base * directory of the user. * <p> * <code>"jdbc:h2:~/" + subname</code> * * @param user the database user on whose behalf the connection is being * made. * @param password the user's password. May be empty * @return a connection object pointing to the database * @throws SQLException if an sql error occurs or the h2 driver could not be * found. */ private static Connection getConnection(String subname, String user, String password) throws SQLException { try { return DriverManager.getConnection("jdbc:h2:~/" + subname, user, password); } catch (SQLException exception) { if (exception.getMessage().contains("No suitable driver found")) { LOG.severe(CLASS_NOT_FOUND_ERROR); } throw exception; } } /** * Make sure that the supplied connection is a h2 database connection. * * @param dbConnection The connection to check * @return the supplied connection if it's an h2 connection * @throws IllegalArgumentException if the connection does not match. */ private static Connection checkConnection(Connection dbConnection) throws IllegalArgumentException { if ("org.h2.jdbc.JdbcConnection".contains(dbConnection.getClass().getName())) { return dbConnection; } else { throw new IllegalArgumentException( "To intialize a h2dbimagematcher you must supply a h2 connection. Did you want to create a DatabaseImageMatcher instead?"); } } }