/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2006-2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * OpenNMS(R) 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.netmgt.vacuumd; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.opennms.core.utils.PropertiesUtils; import org.opennms.core.utils.ThreadCategory; import org.opennms.core.utils.PropertiesUtils.SymbolTable; import org.opennms.netmgt.config.VacuumdConfigFactory; import org.opennms.netmgt.config.vacuumd.Action; import org.opennms.netmgt.config.vacuumd.ActionEvent; import org.opennms.netmgt.config.vacuumd.Assignment; import org.opennms.netmgt.config.vacuumd.AutoEvent; import org.opennms.netmgt.config.vacuumd.Automation; import org.opennms.netmgt.config.vacuumd.Trigger; import org.opennms.netmgt.model.events.EventBuilder; import org.opennms.netmgt.model.events.Parameter; import org.opennms.netmgt.scheduler.ReadyRunnable; import org.opennms.netmgt.scheduler.Schedule; import org.opennms.netmgt.xml.event.Event; /** * This class used to process automations configured in * the vacuumd-configuration.xml file. Automations are * identified by a name and they reference * Triggers and Actions by name, as well. Autmations also * have an interval attribute that determines how often * they run. * * @author <a href="mailto:david@opennms.org">David Hustace</a> * @version $Id: $ */ public class AutomationProcessor implements ReadyRunnable { private final Automation m_automation; private final TriggerProcessor m_trigger; private final ActionProcessor m_action; /** * @deprecated Associate {@link Automation} objects with {@link ActionEvent} instances instead. */ private final AutoEventProcessor m_autoEvent; private final ActionEventProcessor m_actionEvent; private volatile Schedule m_schedule; private volatile boolean m_ready = false; static class TriggerProcessor { private final Trigger m_trigger; public TriggerProcessor(String automationName, Trigger trigger) { m_trigger = trigger; } public ThreadCategory log() { return ThreadCategory.getInstance(getClass()); } public Trigger getTrigger() { return m_trigger; } public boolean hasTrigger() { return m_trigger != null; } public String getTriggerSQL() { if (hasTrigger()) { return getTrigger().getStatement().getContent(); } else { return null; } } public String getName() { return getTrigger().getName(); } public String toString() { return m_trigger == null ? "<No-Trigger>" : m_trigger.getName(); } ResultSet runTriggerQuery() throws SQLException { try { if (!hasTrigger()) { return null; } Connection conn = Transaction.getConnection(m_trigger.getDataSource()); Statement triggerStatement = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); Transaction.register(triggerStatement); ResultSet triggerResultSet = triggerStatement.executeQuery(getTriggerSQL()); Transaction.register(triggerResultSet); return triggerResultSet; } catch (SQLException e) { log().warn("Error executing trigger "+getName(), e); throw e; } } /** * This method verifies that the number of rows in the result set of the trigger * match the defined operation in the config. For example, if the user has specified * that the trigger-rows = 5 and the operator ">", the automation will only run * if the result rows is greater than 5. * @param trigRowCount * @param trigOp * @param resultRows * @param processor TODO */ public boolean triggerRowCheck(int trigRowCount, String trigOp, int resultRows) { if (trigRowCount == 0 || trigOp == null) { log().debug("triggerRowCheck: trigger has no row-count restrictions: operator is: "+trigOp+", row-count is: "+trigRowCount); return true; } log().debug("triggerRowCheck: Verifying trigger resulting row count " +resultRows+" is "+trigOp+" "+trigRowCount); boolean runAction = false; if ("<".equals(trigOp)) { if (resultRows < trigRowCount) runAction = true; } else if ("<=".equals(trigOp)) { if (resultRows <= trigRowCount) runAction = true; } else if ("=".equals(trigOp)) { if (resultRows == trigRowCount) runAction = true; } else if (">=".equals(trigOp)) { if (resultRows >= trigRowCount) runAction = true; } else if (">".equals(trigOp)) { if (resultRows > trigRowCount) runAction = true; } log().debug("Row count verification is: "+runAction); return runAction; } } static class TriggerResults { private final TriggerProcessor m_trigger; private final ResultSet m_resultSet; private final boolean m_successful; public TriggerResults(TriggerProcessor trigger, ResultSet set, boolean successful) { m_trigger = trigger; m_resultSet = set; m_successful = successful; } public boolean hasTrigger() { return m_trigger.hasTrigger(); } public ResultSet getResultSet() { return m_resultSet; } public boolean isSuccessful() { return m_successful; } } static class ActionProcessor { private final String m_automationName; private final Action m_action; public ActionProcessor(String automationName, Action action) { m_automationName = automationName; m_action = action; } public boolean hasAction() { return m_action != null; } public Action getAction() { return m_action; } public ThreadCategory log() { return ThreadCategory.getInstance(getClass()); } String getActionSQL() { return getAction().getStatement().getContent(); } PreparedStatement createPreparedStatement() throws SQLException { String actionJDBC = getActionSQL().replaceAll("\\$\\{\\w+\\}", "?"); log().debug("createPrepareStatement: This action SQL: "+getActionSQL()+"\nTurned into this: "+actionJDBC); Connection conn = Transaction.getConnection(m_action.getDataSource()); PreparedStatement stmt = conn.prepareStatement(actionJDBC); Transaction.register(stmt); return stmt; } /** * Returns an ArrayList containing the names of column defined * as tokens in the action statement defined in the config. If no * tokens are found, an empty list is returned. * @param targetString * @return */ public List<String> getActionColumns() { return getTokenizedColumns(getActionSQL()); } private List<String> getTokenizedColumns(String targetString) { // The \w represents a "word" charactor String expression = "\\$\\{(\\w+)\\}"; Pattern pattern = Pattern.compile(expression); Matcher matcher = pattern.matcher(targetString); log().debug("getTokenizedColumns: processing string: "+targetString); List<String> tokens = new ArrayList<String>(); int count = 0; while (matcher.find()) { count++; log().debug("getTokenizedColumns: Token "+count+": "+matcher.group(1)); tokens.add(matcher.group(1)); } return tokens; } void assignStatementParameters(PreparedStatement stmt, ResultSet rs) throws SQLException { List<String> actionColumns = getTokenizedColumns(getActionSQL()); Iterator<String> it = actionColumns.iterator(); String actionColumnName = null; int i=0; while (it.hasNext()) { actionColumnName = (String)it.next(); stmt.setObject(++i, rs.getObject(actionColumnName)); } } /** * Counts the number of tokens in an Action Statement. * @param targetString * @return */ public int getTokenCount(String targetString) { // The \w represents a "word" charactor String expression = "(\\$\\{\\w+\\})"; Pattern pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(targetString); log().debug("getTokenCount: processing string: "+targetString); int count = 0; while (matcher.find()) { count++; log().debug("getTokenCount: Token "+count+": "+matcher.group(1)); } return count; } boolean execute() throws SQLException { //No trigger defined, just running the action. if (getTokenCount(getActionSQL()) != 0) { log().info("execute: not running action: "+m_action.getName()+". Action contains tokens in an automation ("+m_automationName+") with no trigger."); return false; } else { //Convert the sql to a PreparedStatement PreparedStatement actionStatement = createPreparedStatement(); actionStatement.executeUpdate(); return true; } } boolean processTriggerResults(TriggerResults triggerResults) throws SQLException { ResultSet triggerResultSet = triggerResults.getResultSet(); triggerResultSet.beforeFirst(); PreparedStatement actionStatement = createPreparedStatement(); //Loop through the select results while (triggerResultSet.next()) { //Convert the sql to a PreparedStatement assignStatementParameters(actionStatement, triggerResultSet); actionStatement.executeUpdate(); } return true; } boolean processAction(TriggerResults triggerResults) throws SQLException { if (triggerResults.hasTrigger()) { return processTriggerResults(triggerResults); } else { return execute(); } } public String getName() { return m_action.getName(); } public String toString() { return m_action.getName(); } public void checkForRequiredColumns(TriggerResults triggerResults) { ResultSet triggerResultSet = triggerResults.getResultSet(); if (!resultSetHasRequiredActionColumns(triggerResultSet, getActionColumns())) { throw new AutomationException("Action "+this+" uses column not defined in trigger: "+triggerResults); } } /** * Helper method that verifies tokens in a config defined action * are available in the ResultSet of the paired trigger * @param rs * @param actionColumns TODO * @param actionSQL * @param processor TODO * @return */ public boolean resultSetHasRequiredActionColumns(ResultSet rs, Collection<String> actionColumns) { log().debug("resultSetHasRequiredActionColumns: Verifying required action columns in trigger ResultSet..."); if (actionColumns.isEmpty()) { return true; } if (rs == null) { return false; } boolean verified = true; String actionColumnName = null; Iterator<String> it = actionColumns.iterator(); while (it.hasNext()) { actionColumnName = (String)it.next(); try { if (rs.findColumn(actionColumnName) > 0) { } } catch (SQLException e) { log().warn("resultSetHasRequiredActionColumns: Trigger ResultSet does NOT have required action columns. Missing: "+actionColumnName); log().warn(e.getMessage()); verified = false; } } return verified; } } /** * @deprecated Use {@link ActionEventProcessor} instead. */ static class AutoEventProcessor { private final String m_automationName; private final AutoEvent m_autoEvent; /** * @deprecated Use {@link ActionEventProcessor} instead. */ public AutoEventProcessor(String automationName, AutoEvent autoEvent) { m_automationName = automationName; m_autoEvent = autoEvent; } public ThreadCategory log() { return ThreadCategory.getInstance(getClass()); } public boolean hasEvent() { return m_autoEvent != null; } public AutoEvent getAutoEvent() { return m_autoEvent; } String getUei() { if (hasEvent()) { return getAutoEvent().getUei().getContent(); } else { return null; } } void send() { if (hasEvent()) { //create and send event log().debug("AutoEventProcessor: Sending auto-event "+getUei()+" for automation "+m_automationName); EventBuilder bldr = new EventBuilder(getUei(), "Automation"); sendEvent(bldr.getEvent()); } else { log().debug("AutoEventProcessor: No auto-event for automation "+m_automationName); } } private void sendEvent(Event event) { Vacuumd.getSingleton().getEventManager().sendNow(event); } } static class SQLExceptionHolder extends RuntimeException { private static final long serialVersionUID = 2479066089399740468L; private final SQLException m_ex; public SQLExceptionHolder(SQLException ex) { m_ex = ex; } public void rethrow() throws SQLException { if (m_ex != null) { throw m_ex; } } } static class ResultSetSymbolTable implements PropertiesUtils.SymbolTable { private final ResultSet m_rs; public ResultSetSymbolTable(ResultSet rs) { m_rs = rs; } public String getSymbolValue(String symbol) { try { return m_rs.getString(symbol); } catch (SQLException e) { throw new SQLExceptionHolder(e); } } } static class InvalidSymbolTable implements PropertiesUtils.SymbolTable { public String getSymbolValue(String symbol) { throw new IllegalArgumentException("token "+symbol+" is not allowed for "+this+" when no trigger is being processed"); } } static class EventAssignment { static final Pattern s_pattern = Pattern.compile("\\$\\{(\\w+)\\}"); private final Assignment m_assignment; public EventAssignment(Assignment assignment) { m_assignment = assignment; } public ThreadCategory log() { return ThreadCategory.getInstance(getClass()); } public void assign(EventBuilder bldr, PropertiesUtils.SymbolTable symbols) { String val = PropertiesUtils.substitute(m_assignment.getValue(), symbols); if (m_assignment.getValue().equals(val) && s_pattern.matcher(val).matches()) { // no substitution was made the value was a token pattern so skip it return; } if ("field".equals(m_assignment.getType())) { bldr.setField(m_assignment.getName(), val); } else { bldr.addParam(m_assignment.getName(), val); } } } static class ActionEventProcessor { private final String m_automationName; private final ActionEvent m_actionEvent; private final List<EventAssignment> m_assignments; public ActionEventProcessor(String automationName, ActionEvent actionEvent) { m_automationName = automationName; m_actionEvent = actionEvent; if (actionEvent != null) { m_assignments = new ArrayList<EventAssignment>(actionEvent.getAssignmentCount()); for(Assignment assignment : actionEvent.getAssignment()) { m_assignments.add(new EventAssignment(assignment)); } } else { m_assignments = null; } } public ThreadCategory log() { return ThreadCategory.getInstance(getClass()); } public boolean hasEvent() { return m_actionEvent != null; } void send() { if (hasEvent()) { // the uei will be set by the event assignments EventBuilder bldr = new EventBuilder(null, "Automation"); buildEvent(bldr, new InvalidSymbolTable()); log().debug("ActionEventProcessor: Sending action-event " + bldr.getEvent().getUei() + " for automation "+m_automationName); sendEvent(bldr.getEvent()); } else { log().debug("ActionEventProcessor: No action-event for automation "+m_automationName); } } private void buildEvent(EventBuilder bldr, SymbolTable symbols) { for(EventAssignment assignment : m_assignments) { assignment.assign(bldr, symbols); } } private void sendEvent(Event event) { Vacuumd.getSingleton().getEventManager().sendNow(event); } void processTriggerResults(TriggerResults triggerResults) throws SQLException { if (!hasEvent()) { log().debug("processTriggerResults: No action-event for automation "+m_automationName); return; } ResultSet triggerResultSet = triggerResults.getResultSet(); triggerResultSet.beforeFirst(); //Loop through the select results while (triggerResultSet.next()) { // the uei will be set by the event assignments EventBuilder bldr = new EventBuilder(null, "Automation"); ResultSetSymbolTable symbols = new ResultSetSymbolTable(triggerResultSet); try { if (m_actionEvent.isAddAllParms() && resultHasColumn(triggerResultSet, "eventParms") ) { bldr.setParms(Parameter.decode(triggerResultSet.getString("eventParms"))); } buildEvent(bldr, symbols); } catch (SQLExceptionHolder holder) { holder.rethrow(); } log().debug("processTriggerResults: Sending action-event " + bldr.getEvent().getUei() + " for automation "+m_automationName); sendEvent(bldr.getEvent()); } } private boolean resultHasColumn(ResultSet resultSet, String columnName) { try { if (resultSet.findColumn(columnName) > 0) { return true; } } catch (SQLException e) { } return false; } public boolean forEachResult() { return m_actionEvent == null ? false : m_actionEvent.getForEachResult(); } void processActionEvent(TriggerResults triggerResults) throws SQLException { if (triggerResults.hasTrigger() && forEachResult()) { processTriggerResults(triggerResults); } else { send(); } } } /** * Public constructor. * * @param automation a {@link org.opennms.netmgt.config.vacuumd.Automation} object. */ public AutomationProcessor(Automation automation) { m_ready = true; m_automation = automation; m_trigger = new TriggerProcessor(m_automation.getName(), VacuumdConfigFactory.getInstance().getTrigger(m_automation.getTriggerName())); m_action = new ActionProcessor(m_automation.getName(), VacuumdConfigFactory.getInstance().getAction(m_automation.getActionName())); m_autoEvent = new AutoEventProcessor(m_automation.getName(), VacuumdConfigFactory.getInstance().getAutoEvent(m_automation.getAutoEventName())); m_actionEvent = new ActionEventProcessor(m_automation.getName(),VacuumdConfigFactory.getInstance().getActionEvent(m_automation.getActionEvent())); } /** * <p>getAction</p> * * @return a {@link org.opennms.netmgt.vacuumd.AutomationProcessor.ActionProcessor} object. */ public ActionProcessor getAction() { return m_action; } /** * <p>getTrigger</p> * * @return a {@link org.opennms.netmgt.vacuumd.AutomationProcessor.TriggerProcessor} object. */ public TriggerProcessor getTrigger() { return m_trigger; } /* (non-Javadoc) * @see java.lang.Runnable#run() */ /** * <p>run</p> */ public void run() { Date startDate = new Date(); log().debug("Start Scheduled automation "+this); if (getAutomation() != null) { setReady(false); try { runAutomation(); } catch (SQLException e) { log().warn("Error running automation: "+getAutomation().getName()+", "+e.getMessage()); } finally { setReady(true); } } log().debug("run: Finished automation "+m_automation.getName()+", started at "+startDate); } /** * Called by the run method to execute the sql statements * of triggers and actions defined for an automation. An * automation may have 0 or 1 trigger and must have 1 action. * If the automation doesn't have a trigger than the action * must not contain any tokens. * * @throws java.sql.SQLException if any. * @return a boolean. */ public boolean runAutomation() throws SQLException { log().debug("runAutomation: "+m_automation.getName()+" running..."); if (hasTrigger()) { log().debug("runAutomation: "+m_automation.getName()+" trigger statement is: "+ m_trigger.getTriggerSQL()); } log().debug("runAutomation: "+m_automation.getName()+" action statement is: "+m_action.getActionSQL()); log().debug("runAutomation: Executing trigger: "+m_automation.getTriggerName()); Transaction.begin(); try { log().debug("runAutomation: Processing automation: "+m_automation.getName()); TriggerResults results = processTrigger(); boolean success = false; if (results.isSuccessful()) { success = processAction(results); } return success; } catch (Throwable e) { Transaction.rollbackOnly(); log().warn("runAutomation: Could not execute automation: "+m_automation.getName(), e); return false; } finally { log().debug("runAutomation: Ending processing of automation: "+m_automation.getName()); Transaction.end(); } } private boolean processAction(TriggerResults triggerResults) throws SQLException { log().debug("runAutomation: running action(s)/actionEvent(s) for : "+m_automation.getName()); //Verfiy the trigger ResultSet returned the required number of rows and the required columns for the action statement m_action.checkForRequiredColumns(triggerResults); if (m_action.processAction(triggerResults)) { m_actionEvent.processActionEvent(triggerResults); m_autoEvent.send(); return true; } else { return false; } } private TriggerResults processTrigger() throws SQLException { if (m_trigger.hasTrigger()) { //get a scrollable ResultSet so that we can count the rows and move back to the //beginning for processing. ResultSet triggerResultSet = m_trigger.runTriggerQuery(); TriggerResults triggerResults = new TriggerResults(m_trigger, triggerResultSet, verifyRowCount(triggerResultSet)); return triggerResults; } else { return new TriggerResults(m_trigger, null, true); } } /** * <p>verifyRowCount</p> * * @param triggerResultSet a {@link java.sql.ResultSet} object. * @return a boolean. * @throws java.sql.SQLException if any. */ protected boolean verifyRowCount(ResultSet triggerResultSet) throws SQLException { if (!m_trigger.hasTrigger()) { return true; } int resultRows; boolean validRows = true; //determine if number of rows required by the trigger row-count and operator were //met by the trigger query, if so we'll run the action resultRows = countRows(triggerResultSet); int triggerRowCount = m_trigger.getTrigger().getRowCount(); String triggerOperator = m_trigger.getTrigger().getOperator(); log().debug("verifyRowCount: Verifying trigger result: "+resultRows+" is "+(triggerOperator == null ? "<null>" : triggerOperator)+" than "+triggerRowCount); if (!m_trigger.triggerRowCheck(triggerRowCount, triggerOperator, resultRows)) validRows = false; return validRows; } /** * Method used to count the rows in a ResultSet. This probably requires * that your ResultSet is scrollable. * * @param rs a {@link java.sql.ResultSet} object. * @throws java.sql.SQLException if any. * @return a int. */ public int countRows(ResultSet rs) throws SQLException { if (rs == null) { return 0; } int rows = 0; while (rs.next()) rows++; rs.beforeFirst(); return rows; } /** * Simple helper method to determine if the targetString contains * any '${token}'s. * * @param targetString a {@link java.lang.String} object. * @return a boolean. */ public boolean containsTokens(String targetString) { return m_action.getTokenCount(targetString) > 0; } /** * <p>getAutomation</p> * * @return Returns the automation. */ public Automation getAutomation() { return m_automation; } /** * <p>isReady</p> * * @return a boolean. */ public boolean isReady() { return m_ready; } /** * <p>getSchedule</p> * * @return Returns the schedule. */ public Schedule getSchedule() { return m_schedule; } /** * <p>setSchedule</p> * * @param schedule The schedule to set. */ public void setSchedule(Schedule schedule) { m_schedule = schedule; } private ThreadCategory log() { return ThreadCategory.getInstance(AutomationProcessor.class); } private boolean hasTrigger() { return m_trigger.hasTrigger(); } /** * <p>setReady</p> * * @param ready a boolean. */ public void setReady(boolean ready) { m_ready = ready; } }