package com.gmail.dpierron.calibre.trook; import com.gmail.dpierron.calibre.configuration.ConfigurationManager; import com.gmail.dpierron.calibre.configuration.DeviceMode; import com.gmail.dpierron.calibre.datamodel.Author; import com.gmail.dpierron.calibre.datamodel.Book; import com.gmail.dpierron.calibre.datamodel.Series; import com.gmail.dpierron.calibre.datamodel.Tag; import com.gmail.dpierron.calibre.opds.JDOMManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jdom2.Element; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.sql.*; import java.util.*; public class TrookSpecificSearchDatabaseManager { private static final int MIN_KEYWORD_LEN = 3; private static File databaseFile = null; private static Connection connection = null; private static Map<String, Long> keywords = new HashMap<String, Long>(); private static List<Book> storedBooks = new LinkedList<Book>(); private static List<Author> storedAuthors = new LinkedList<Author>(); private static List<Series> storedSeries = new LinkedList<Series>(); private static List<Tag> storedTags = new LinkedList<Tag>(); private static long keywordCounter = 0; private static long resultCounter = 0; private static final Logger logger = LogManager.getLogger(TrookSpecificSearchDatabaseManager.class); public static void setDatabaseFile(File dbFile) { databaseFile = dbFile; } public static File getDatabaseFile() { return databaseFile; } public static Connection getConnection() { if (connection == null) { initConnection(); } return connection; } public static void closeConnection() { if (connection != null) { try { connection.close(); } catch (SQLException e) { // Ignore any exception } } } public static boolean databaseExists() { if (databaseFile == null) return false; if (!databaseFile.exists()) return false; return true; } private static void initConnection() { try { Class.forName("org.sqlite.JDBC"); if (databaseFile == null) return; if (databaseExists()) databaseFile.delete(); String url = databaseFile.toURI().getPath(); connection = DriverManager.getConnection("jdbc:sqlite:" + url); Statement stat = connection.createStatement(); // drop the indexes stat.executeUpdate("DROP INDEX IF EXISTS KEYWORDS_RESULTS_RELATION_INDEX_ON_RESULTS;"); stat.executeUpdate("DROP INDEX IF EXISTS KEYWORDS_RESULTS_RELATION_INDEX_ON_KEYWORD;"); stat.executeUpdate("DROP INDEX IF EXISTS RESULTS_INDEX;"); stat.executeUpdate("DROP INDEX IF EXISTS KEYWORDS_INDEX;"); // drop the tables stat.executeUpdate("DROP TABLE IF EXISTS keywords_results_relation"); stat.executeUpdate("DROP TABLE IF EXISTS results"); stat.executeUpdate("DROP TABLE IF EXISTS keywords"); // create the tables stat.executeUpdate("CREATE TABLE keywords (keyword_id INTEGER PRIMARY KEY, keyword_value TEXT);"); stat.executeUpdate("CREATE TABLE results (result_id INTEGER PRIMARY KEY, result_type TEXT, result_entry TEXT);"); stat.executeUpdate("CREATE TABLE keywords_results_relation (keyword_id INTEGER, result_id INTEGER);"); // create the indexes stat.executeUpdate("CREATE UNIQUE INDEX KEYWORDS_INDEX ON keywords(keyword_id ASC);"); stat.executeUpdate("CREATE UNIQUE INDEX RESULTS_INDEX ON results(result_id ASC);"); stat.executeUpdate("CREATE INDEX KEYWORDS_RESULTS_RELATION_INDEX_ON_KEYWORD ON keywords_results_relation(keyword_id ASC);"); stat.executeUpdate("CREATE INDEX KEYWORDS_RESULTS_RELATION_INDEX_ON_RESULTS ON keywords_results_relation(result_id ASC);"); } catch (ClassNotFoundException e) { logger.error(e); } catch (SQLException e) { logger.error(e); } } private static long addResult(String opdsEntry, ResultType type) throws SQLException { PreparedStatement ps = getConnection().prepareStatement("INSERT INTO results(result_id, result_type, result_entry) values (?, ?, ?);"); ps.setLong(1, ++resultCounter); ps.setString(2, type.name().toUpperCase(Locale.ENGLISH)); ps.setString(3, opdsEntry); ps.execute(); return resultCounter; } private static long addKeyword(String keyword) throws SQLException { // check if keyword already exist (we test in memory to be quicker) if (keywords.containsKey(keyword)) return keywords.get(keyword); else { PreparedStatement ps = getConnection().prepareStatement("INSERT INTO keywords(keyword_id, keyword_value) values (?, ?);"); ps.setLong(1, ++keywordCounter); ps.setString(2, keyword); ps.execute(); keywords.put(keyword, keywordCounter); } return keywordCounter; } private static void addKeywordResultRelation(long keywordId, long resultId) throws SQLException { PreparedStatement ps = getConnection().prepareStatement("INSERT INTO keywords_results_relation(keyword_id, result_id) values (?, ?);"); ps.setLong(1, keywordId); ps.setLong(2, resultId); ps.execute(); } private static List<String> keywordize(String pKeywordString) { List<String> keywords = new LinkedList<String>(); List<String> result = new LinkedList<String>(); String keywordString = pKeywordString.toUpperCase(Locale.ENGLISH); StringBuffer currentKeyword = null; for (int pos = 0; pos < keywordString.length(); pos++) { char c = keywordString.charAt(pos); if (!Character.isLetterOrDigit(c)) { // skip and break keywords at non letter or digits if (currentKeyword != null) { // process current keyword keywords.add(currentKeyword.toString()); currentKeyword = null; } continue; } if (currentKeyword == null) currentKeyword = new StringBuffer(); currentKeyword.append(c); } // process the last keyword if (currentKeyword != null) { keywords.add(currentKeyword.toString()); currentKeyword = null; } // make packages with keywords, in increasing size for (int size = 1; size <= keywords.size(); size++) { int pos = 0; while (pos + size <= keywords.size()) { StringBuffer keywordPackage = new StringBuffer(); for (int i = pos; i < pos + size; i++) { keywordPackage.append(keywords.get(i)); keywordPackage.append(' '); } String s = keywordPackage.toString(); result.add(s.substring(0, s.length() - 1)); pos++; } } // add the "whole string" keyword result.add(keywordString); return result; } public static void addEntry(String opdsEntry, ResultType type, String... keywordStrings) throws SQLException { // add the result long resultId = addResult(opdsEntry, type); for (String keywordString : keywordStrings) { // process the keyword string List<String> keywords = keywordize(keywordString); for (String keyword : keywords) { if (keyword.length() >= MIN_KEYWORD_LEN) { long keywordId = addKeyword(keyword); addKeywordResultRelation(keywordId, resultId); } } } } private static void addEntryToTrookSearchDatabase(Element entry, ResultType type, String... keywords) { try { StringWriter sw = new StringWriter(); JDOMManager.getCompactXML().output(entry, sw); String opdsEntry = sw.getBuffer().toString(); addEntry(opdsEntry, type, keywords); } catch (IOException e) { logger.warn(e.getMessage()); } catch (SQLException e) { logger.warn(e.getMessage()); } } public static void addAuthor(Author item, Element entry) { if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) { if (!storedAuthors.contains(item)) { logger.debug("adding result for " + item); String keywords = item.getName(); addEntryToTrookSearchDatabase(entry, ResultType.AUTHOR, keywords); storedAuthors.add(item); } } } public static void addSeries(Series item, Element entry) { if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) { if (!storedSeries.contains(item)) { logger.debug("adding result for " + item); String keywords = item.getName(); // TODO do this after Doug has added support for noise words filtering in series // String noNoise = item.getTitleForSort(ConfigurationManager.getCurrentProfile().getBookLanguageTag()); addEntryToTrookSearchDatabase(entry, ResultType.SERIES, keywords); storedSeries.add(item); } } } public static void addTag(Tag item, Element entry) { if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) { if (!storedTags.contains(item)) { logger.debug("adding result for " + item); String keywords = item.getName(); addEntryToTrookSearchDatabase(entry, ResultType.TAG, keywords); storedTags.add(item); } } } public static void addBook(Book item, Element entry) { if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) { if (!storedBooks.contains(item)) { logger.debug("adding result for " + item); String keywords = item.getTitle(); String noNoise = item.getTitle_Sort(); addEntryToTrookSearchDatabase(entry, ResultType.BOOK, keywords, noNoise); storedBooks.add(item); } } } }