/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.tools.usagestats; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.WeakHashMap; import java.util.logging.Level; import com.rapidminer.tools.LogService; import com.rapidminer.tools.usagestats.ActionStatisticsCollector.Key; /** * Data access object for the call to action database * * @author Jonas Wilms-Pfau * @since 7.5.0 */ enum CtaDao { INSTANCE; /** Insert a new event with the current timestamp */ private static final String INSERT_EVENT_QUERY = "INSERT INTO event(type,value,argument,count) VALUES (?,?,?,?)"; /** Insert a new rule trigger event with the current timestamp */ private static final String INSERT_RULE_QUERY = "INSERT INTO rule(id, action) VALUES (?,?)"; /** This query returns an empty result set, if the offset is larger than the event count */ private static final String SELECT_OVER_ONE_MILLION = "SELECT timestamp FROM event ORDER BY timestamp DESC LIMIT 1 OFFSET 1000000"; /** Delete events older than the given timestamp */ private static final String DELETE_OLDER_THAN = "DELETE FROM event WHERE timestamp < ?"; /** Delete events older than one month */ private static final String DELETE_OLDER_ONE_MONTH = "DELETE FROM event WHERE timestamp < DATEADD('MONTH', -1, NOW())"; /** * Rule verification query cache, weak since old rules might be deleted */ private Map<Rule, List<PreparedStatement>> queryCache = new WeakHashMap<>(); private PreparedStatement insertEvent = null; private PreparedStatement insertRule = null; private PreparedStatement selectOverOneMillion = null; private PreparedStatement deleteOlderThan = null; private PreparedStatement deleteOlderOneMonth = null; private CtaDao() { try { Connection connection = CtaDataSource.INSTANCE.getConnection(); insertEvent = connection.prepareStatement(INSERT_EVENT_QUERY); insertRule = connection.prepareStatement(INSERT_RULE_QUERY); selectOverOneMillion = connection.prepareStatement(SELECT_OVER_ONE_MILLION); deleteOlderThan = connection.prepareStatement(DELETE_OLDER_THAN); deleteOlderOneMonth = connection.prepareStatement(DELETE_OLDER_ONE_MONTH); } catch (SQLException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.usagestats.CtaDao.init.failure", e); } } /** * Store a rule triggered event * * @param rule * The Rule that has been triggered * @param action * The users action * @throws SQLException */ public void triggered(Rule rule, String action) throws SQLException { insertRule.setString(1, rule.getId()); insertRule.setString(2, action); insertRule.execute(); } /** * Store events * * @param events * @throws SQLException */ public void storeEvents(Map<Key, Long> events) throws SQLException { for (Entry<Key, Long> event : events.entrySet()) { Key key = event.getKey(); insertEvent.setString(1, key.getType()); insertEvent.setString(2, key.getValue()); insertEvent.setString(3, key.getArg()); insertEvent.setLong(4, event.getValue()); insertEvent.addBatch(); // We do not clear the parameter since we override them anyway } // This is also working for 0 batches insertEvent.executeBatch(); } /** * Verifies the rules * * @param rule * @return true if the rule is fulfilled * @throws SQLException */ public boolean verify(Rule rule) throws SQLException { // An empty rule should not be triggered boolean isValid = false; List<PreparedStatement> statements = getStatements(rule); int index = -1; for (PreparedStatement statement : statements) { index++; ResultSet result = statement.executeQuery(); if (result.next()) { isValid = result.getBoolean(1); } // Abort if one condition is unsatisfied if (!isValid) { // Fail earlier next time if (index > 0) { Collections.swap(statements, 0, index); } return isValid; } } return isValid; } /** * Return a list of prepared statements for the rule * * @param rule * @return * @throws SQLException */ private List<PreparedStatement> getStatements(Rule rule) throws SQLException { List<PreparedStatement> statements = queryCache.get(rule); // Prepare the statements if necessary if (statements == null) { statements = new ArrayList<>(); Connection connection = CtaDataSource.INSTANCE.getConnection(); for (String query : rule.getQueries()) { statements.add(connection.prepareStatement(query)); } queryCache.put(rule, statements); } return statements; } /** * Cleans up the Database * <ul> * <li>Delete all events older than a month</li> * <li>Delete all events over 1.000.000</li> * </ul> * * @throws SQLException */ public void cleanUpDatabase() throws SQLException { deleteOlderOneMonth.executeUpdate(); ResultSet oldResult = selectOverOneMillion.executeQuery(); if (oldResult.next()) { Timestamp old = oldResult.getTimestamp(1); deleteOlderThan.setTimestamp(1, old); deleteOlderThan.executeUpdate(); } } }