/** * 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.SQLException; import java.util.Collection; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import javax.swing.SwingUtilities; import com.rapidminer.RapidMiner; import com.rapidminer.RapidMiner.ExecutionMode; import com.rapidminer.gui.tools.BrowserPopup; import com.rapidminer.gui.tools.bubble.WindowChoreographer; import com.rapidminer.studio.internal.RuleProviderRegistry; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.usagestats.ActionStatisticsCollector.Key; /** * Collects statistics and displays messages to the user if certain conditions are fulfilled * * @author Jonas Wilms-Pfau, Marco Boeck * @since 7.5.0 */ public enum CallToActionScheduler { /** the singleton instance */ INSTANCE; /** Pull every 5 seconds */ private static final int DELAY = 5; /** Start immediately */ private static final int INITIAL_DELAY = 0; /** Clean on start-up */ private static final int CLEAN_DELAY = 0; /** Clean every day */ private static final int CLEAN_INTERVAL = 1; /** Skip iteration if still running */ private static AtomicBoolean IS_RUNNING = new AtomicBoolean(false); private static WindowChoreographer choreographer = new WindowChoreographer(); private ScheduledExecutorService exec; /** * Trigger class initialization. If not in {@link ExecutionMode#UI}, does nothing. */ public void init() { // Only initialize in UI mode if (!RapidMiner.getExecutionMode().equals(ExecutionMode.UI)) { return; } // Register rule provider RuleProviderRegistry.INSTANCE.register(new LocalRuleProvider(), RuleProviderRegistry.PRECEDENCE_LOCAL); RuleProviderRegistry.INSTANCE.register(new RemoteRuleProvider(), RuleProviderRegistry.PRECEDENCE_REMOTE); LogService.getRoot().log(Level.FINE, "com.rapidminer.tools.usagestats.CallToActionScheduler.init.start"); exec = Executors.newSingleThreadScheduledExecutor(); // If any execution of the task encounters an exception, subsequent executions are // suppressed exec.scheduleAtFixedRate(new Runnable() { @Override public void run() { if (IS_RUNNING.compareAndSet(false, true)) { try { persistEvents(); checkRules(); } finally { IS_RUNNING.set(false); } } } }, INITIAL_DELAY, DELAY, TimeUnit.SECONDS); exec.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { CtaDao.INSTANCE.cleanUpDatabase(); } catch (SQLException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.usagestats.CallToActionScheduler.db.clean.failed", e); } } }, CLEAN_DELAY, CLEAN_INTERVAL, TimeUnit.DAYS); } /** * Tries to terminate the CTA scheduler. Will wait for a couple seconds for the currently * running task to complete before aborting. */ public void shutdown() { boolean terminatedGracefully = false; try { LogService.getRoot().log(Level.INFO, "com.rapidminer.tools.usagestats.CallToActionScheduler.shutdown.start"); exec.shutdown(); persistEvents(); terminatedGracefully = exec.awaitTermination(DELAY, TimeUnit.SECONDS); } catch (InterruptedException e) { // do nothing } finally { if (terminatedGracefully) { LogService.getRoot().log(Level.FINE, "com.rapidminer.tools.usagestats.CallToActionScheduler.shutdown.finish_success"); } else { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.usagestats.CallToActionScheduler.shutdown.finish_failure"); } } } /** * Pull & persist the current collected events */ private void persistEvents() { Map<Key, Long> events = CtaEventAggregator.INSTANCE.pullEvents(); // There is no white list yet try { CtaDao.INSTANCE.storeEvents(events); } catch (SQLException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.usagestats.CallToActionScheduler.storing.failure", e); throw new RuntimeException("The database died, shutting down CTA.", e); } } /** * Fetch Rules and verifies them. If fulfilled, display the message and store the users * reaction. */ private void checkRules() { Collection<VerifiableRule> rules = RuleService.INSTANCE.getRules(); for (VerifiableRule rule : rules) { // check if rule is fulfilled, if it is, create CTA popup and show it if (rule.isRuleFulfilled()) { LogService.getRoot().log(Level.FINE, "com.rapidminer.tools.usagestats.CallToActionScheduler.rule.verification.triggered", rule.getRule().getId()); // Display the CTA final BrowserPopup cta = new BrowserPopup(rule.getRule().getMessage()); rule.setDisplaying(true); SwingUtilities.invokeLater(() -> { choreographer.addWindow(cta); }); // wait for results in separate thread (user may not click on CTA for ages) new Thread(new Runnable() { @Override public void run() { // Store the triggered state try { String result = cta.get(); rule.setDisplaying(false); CtaDao.INSTANCE.triggered(rule.getRule(), result); ActionStatisticsCollector.INSTANCE.logCtaRuleTriggered(rule.getRule().getId(), result); } catch (SQLException e) { LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.tools.usagestats.CallToActionScheduler.rule.triggered.storage.failed", rule.getRule().getId()), e); } } }).start(); } } } }