/*******************************************************************************
* 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.config;
import java.beans.PropertyEditorSupport;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.ValidationException;
import org.opennms.core.utils.ConfigFileConstants;
import org.opennms.core.utils.MatchTable;
import org.opennms.core.utils.PropertiesUtils;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.core.xml.CastorUtils;
import org.opennms.netmgt.config.translator.Assignment;
import org.opennms.netmgt.config.translator.EventTranslationSpec;
import org.opennms.netmgt.config.translator.EventTranslatorConfiguration;
import org.opennms.netmgt.config.translator.Mapping;
import org.opennms.netmgt.config.translator.Translation;
import org.opennms.netmgt.config.translator.Value;
import org.opennms.netmgt.eventd.EventUtil;
import org.opennms.netmgt.utils.SingleResultQuerier;
import org.opennms.netmgt.xml.event.Event;
import org.opennms.netmgt.xml.event.Parm;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.PropertyAccessorFactory;
/**
* This is the singleton class used to load the configuration from the
* passive-status-configuration.xml. This provides convenience methods to get the configured
* categories and their information, add/delete categories from category groups.
*
* <strong>Note: </strong>Users of this class should make sure the
* <em>init()</em> is called before calling any other method to ensure the
* config is loaded before accessing other convenience methods.
*
* @author <a href="mailto:david@opennms.org">David Hustace </a>
* @author <a href="http://www.opennms.org/">OpenNMS </a>
*/
public final class EventTranslatorConfigFactory implements EventTranslatorConfig {
/**
* The singleton instance of this factory
*/
private static EventTranslatorConfig m_singleton = null;
/**
* The config class loaded from the config file
*/
private EventTranslatorConfiguration m_config;
private List<TranslationSpec> m_translationSpecs;
/**
* This member is set to true if the configuration file has been loaded.
*/
private static boolean m_loaded = false;
/**
* connection factory for use with sql-value
*/
private DataSource m_dbConnFactory = null;
/**
* Private constructor
*
* @exception java.io.IOException
* Thrown if the specified config file cannot be read
* @exception org.exolab.castor.xml.MarshalException
* Thrown if the file does not conform to the schema.
* @exception org.exolab.castor.xml.ValidationException
* Thrown if the contents do not match the required schema.
*
*/
private EventTranslatorConfigFactory(String configFile, DataSource dbConnFactory) throws IOException, MarshalException, ValidationException {
InputStream stream = null;
try {
stream = new FileInputStream(configFile);
unmarshall(stream, dbConnFactory);
} finally {
if (stream != null) {
IOUtils.closeQuietly(stream);
}
}
}
/**
* <p>Constructor for EventTranslatorConfigFactory.</p>
*
* @param rdr a {@link java.io.Reader} object.
* @param dbConnFactory a {@link javax.sql.DataSource} object.
* @throws org.exolab.castor.xml.MarshalException if any.
* @throws org.exolab.castor.xml.ValidationException if any.
*/
public EventTranslatorConfigFactory(InputStream rdr, DataSource dbConnFactory) throws MarshalException, ValidationException {
unmarshall(rdr, dbConnFactory);
}
private synchronized void unmarshall(InputStream stream, DataSource dbConnFactory) throws MarshalException, ValidationException {
m_config = CastorUtils.unmarshal(EventTranslatorConfiguration.class, stream);
m_dbConnFactory = dbConnFactory;
}
private synchronized void unmarshall(InputStream stream) throws MarshalException, ValidationException {
unmarshall(stream, null);
}
/**
* Simply marshals the config without messing with the singletons.
*
* @throws java.lang.Exception if any.
*/
public void update() throws Exception {
synchronized (this) {
File cfgFile = ConfigFileConstants.getFile(ConfigFileConstants.TRANSLATOR_CONFIG_FILE_NAME);
InputStream stream = null;
try {
stream = new FileInputStream(cfgFile);
unmarshall(stream);
} finally {
if (stream != null) {
IOUtils.closeQuietly(stream);
}
}
}
}
/**
* Load the config from the default config file and create the singleton
* instance of this factory.
*
* @exception java.io.IOException
* Thrown if the specified config file cannot be read
* @exception org.exolab.castor.xml.MarshalException
* Thrown if the file does not conform to the schema.
* @exception org.exolab.castor.xml.ValidationException
* Thrown if the contents do not match the required schema.
* @throws java.lang.ClassNotFoundException if any.
* @throws java.io.IOException if any.
* @throws org.exolab.castor.xml.MarshalException if any.
* @throws org.exolab.castor.xml.ValidationException if any.
* @throws java.sql.SQLException if any.
* @throws java.beans.PropertyVetoException if any.
*/
public static synchronized void init() throws IOException, MarshalException, ValidationException, ClassNotFoundException, SQLException, PropertyVetoException {
if (m_loaded) {
// init already called - return
// to reload, reload() will need to be called
return;
}
DataSourceFactory.init();
File cfgFile = ConfigFileConstants.getFile(ConfigFileConstants.TRANSLATOR_CONFIG_FILE_NAME);
m_singleton = new EventTranslatorConfigFactory(cfgFile.getPath(), DataSourceFactory.getInstance());
m_loaded = true;
}
/**
* Reload the config from the default config file
*
* @exception java.io.IOException
* Thrown if the specified config file cannot be read/loaded
* @exception org.exolab.castor.xml.MarshalException
* Thrown if the file does not conform to the schema.
* @exception org.exolab.castor.xml.ValidationException
* Thrown if the contents do not match the required schema.
* @throws java.lang.ClassNotFoundException if any.
* @throws java.io.IOException if any.
* @throws org.exolab.castor.xml.MarshalException if any.
* @throws org.exolab.castor.xml.ValidationException if any.
* @throws java.sql.SQLException if any.
* @throws java.beans.PropertyVetoException if any.
*/
public static synchronized void reload() throws IOException, MarshalException, ValidationException, ClassNotFoundException, SQLException, PropertyVetoException {
m_singleton = null;
m_loaded = false;
init();
}
/**
* Return the singleton instance of this factory.
*
* @return The current factory instance.
* @throws java.lang.IllegalStateException
* Thrown if the factory has not yet been initialized.
*/
public static synchronized EventTranslatorConfig getInstance() {
if (!m_loaded)
throw new IllegalStateException("getInstance: The factory has not been initialized");
return m_singleton;
}
/**
* <p>setInstance</p>
*
* @param singleton a {@link org.opennms.netmgt.config.EventTranslatorConfig} object.
*/
public static void setInstance(EventTranslatorConfig singleton) {
m_singleton=singleton;
m_loaded=true;
}
private ThreadCategory log() {
return ThreadCategory.getInstance(EventTranslatorConfig.class);
}
/**
* Return the PassiveStatus configuration.
*
* @return the PassiveStatus configuration
*/
private synchronized EventTranslatorConfiguration getConfig() {
return m_config;
}
/*
* (non-Javadoc)
* @see org.opennms.netmgt.config.PassiveStatusConfig#getUEIList()
*/
/**
* <p>getUEIList</p>
*
* @return a {@link java.util.List} object.
*/
public List<String> getUEIList() {
return getTranslationUEIs();
}
private List<String> getTranslationUEIs() {
Translation translation = getConfig().getTranslation();
if (translation == null)
return Collections.emptyList();
List<String> ueis = new ArrayList<String>();
for (EventTranslationSpec event : translation.getEventTranslationSpecCollection()) {
ueis.add(event.getUei());
}
return ueis;
}
static class TranslationFailedException extends RuntimeException {
private static final long serialVersionUID = -7219413891842193464L;
TranslationFailedException(String msg) {
super(msg);
}
}
/** {@inheritDoc} */
public boolean isTranslationEvent(Event e) {
for (TranslationSpec spec : getTranslationSpecs()) {
if (spec.matches(e))
return true;
}
return false;
}
/** {@inheritDoc} */
public List<Event> translateEvent(Event e) {
ArrayList<Event> events = new ArrayList<Event>();
for (TranslationSpec spec : getTranslationSpecs()) {
events.addAll(spec.translate(e));
}
return events;
}
private List<TranslationSpec> getTranslationSpecs() {
if (m_translationSpecs == null)
m_translationSpecs = constructTranslationSpecs();
return m_translationSpecs;
}
private List<TranslationSpec> constructTranslationSpecs() {
List<TranslationSpec> specs = new ArrayList<TranslationSpec>();
for (EventTranslationSpec eventTrans : m_config.getTranslation().getEventTranslationSpecCollection()) {
specs.add(new TranslationSpec(eventTrans));
}
return specs;
}
class TranslationSpec {
private EventTranslationSpec m_spec;
private List<TranslationMapping> m_translationMappings;
TranslationSpec(EventTranslationSpec spec) {
m_spec = spec;
m_translationMappings = null; // lazy init
}
public List<Event> translate(Event e) {
// short circuit here is the uei doesn't match
if (!ueiMatches(e)) return Collections.emptyList();
// uei matches now go thru the mappings
ArrayList<Event> events = new ArrayList<Event>();
for (TranslationMapping mapping : getTranslationMappings()) {
Event translatedEvent = mapping.translate(e);
if (translatedEvent != null)
events.add(translatedEvent);
}
return events;
}
String getUei() { return m_spec.getUei(); }
public EventTranslationSpec getEventTranslationSpec() {
return m_spec;
}
private List<TranslationMapping> constructTranslationMappings() {
if (m_spec.getMappings() == null) return Collections.emptyList();
List<Mapping> mappings = m_spec.getMappings().getMappingCollection();
List<TranslationMapping> transMaps = new ArrayList<TranslationMapping>(mappings.size());
for (Mapping mapping : mappings) {
TranslationMapping transMap = new TranslationMapping(mapping);
transMaps.add(transMap);
}
return Collections.unmodifiableList(transMaps);
}
List<TranslationMapping> getTranslationMappings() {
if (m_translationMappings == null)
m_translationMappings = constructTranslationMappings();
return Collections.unmodifiableList(m_translationMappings);
}
boolean matches(Event e) {
// short circuit if the eui doesn't match
if (!ueiMatches(e)) {
if (log().isDebugEnabled()) {
log().debug("TransSpec.matches: No match comparing spec UEI: "+m_spec.getUei()+" with event UEI: "+e.getUei());
}
return false;
}
// uei matches to go thru the mappings
log().debug("TransSpec.matches: checking mappings for spec.");
for (TranslationMapping transMap : getTranslationMappings()) {
if (transMap.matches(e))
return true;
}
return false;
}
private boolean ueiMatches(Event e) {
return e.getUei().equals(m_spec.getUei())
|| m_spec.getUei().endsWith("/")
&& e.getUei().startsWith(m_spec.getUei());
}
}
class TranslationMapping {
Mapping m_mapping;
List<AssignmentSpec> m_assignments;
TranslationMapping(Mapping mapping) {
m_mapping = mapping;
m_assignments = null; // lazy init
}
public Event translate(Event srcEvent) {
// if the event doesn't match the mapping then don't apply the translation
if (!matches(srcEvent)) return null;
Event targetEvent = cloneEvent(srcEvent);
for (AssignmentSpec assignSpec : getAssignmentSpecs()) {
assignSpec.apply(srcEvent, targetEvent);
}
targetEvent.setSource(TRANSLATOR_NAME);
return targetEvent;
}
private Event cloneEvent(Event srcEvent) {
Event clonedEvent = EventUtil.cloneEvent(srcEvent);
/* since alarmData and severity are computed based on translated information in
* eventd using the data from eventconf, we unset it here to eventd
* can reset to the proper new settings.
*/
clonedEvent.setAlarmData(null);
clonedEvent.setSeverity(null);
/* the reasoning for alarmData and severity also applies to description (see NMS-4038). */
clonedEvent.setDescr(null);
return clonedEvent;
}
public Mapping getMapping() {
return m_mapping;
}
private List<AssignmentSpec> getAssignmentSpecs() {
if (m_assignments == null)
m_assignments = constructAssignmentSpecs();
return m_assignments;
}
private List<AssignmentSpec> constructAssignmentSpecs() {
Mapping mapping = getMapping();
List<AssignmentSpec> assignments = new ArrayList<AssignmentSpec>();
for (Assignment assign : mapping.getAssignmentCollection()) {
AssignmentSpec assignSpec =
("parameter".equals(assign.getType()) ?
(AssignmentSpec)new ParameterAssignmentSpec(assign) :
(AssignmentSpec)new FieldAssignmentSpec(assign)
);
assignments.add(assignSpec);
}
return assignments;
}
private boolean assignmentsMatch(Event e) {
AssignmentSpec assignSpec = null;
for (Iterator<AssignmentSpec> it = getAssignmentSpecs().iterator(); it.hasNext();) {
assignSpec = it.next();
if (!assignSpec.matches(e)) {
if (log().isDebugEnabled()) {
log().debug("TranslationMapping.assignmentsMatch: assignmentSpec: "+assignSpec.getAttributeName()+" doesn't match.");
}
return false;
}
}
if (log().isDebugEnabled()) {
log().debug("TranslationMapping.assignmentsMatch: assignmentSpec: "+assignSpec.getAttributeName()+" matches!");
}
return true;
}
boolean matches(Event e) {
return assignmentsMatch(e);
}
}
abstract class AssignmentSpec {
private Assignment m_assignment;
private ValueSpec m_valueSpec;
AssignmentSpec(Assignment assignment) {
m_assignment = assignment;
m_valueSpec = null; // lazy init
}
public void apply(Event srcEvent, Event targetEvent) {
setValue(targetEvent, getValueSpec().getResult(srcEvent));
}
private Assignment getAssignment() { return m_assignment; }
protected String getAttributeName() { return getAssignment().getName(); }
private ValueSpec constructValueSpec() {
Value val = getAssignment().getValue();
return EventTranslatorConfigFactory.this.getValueSpec(val);
}
protected abstract void setValue(Event targetEvent, String value);
private ValueSpec getValueSpec() {
if (m_valueSpec == null)
m_valueSpec = constructValueSpec();
return m_valueSpec;
}
boolean matches(Event e) {
return getValueSpec().matches(e);
}
}
class FieldAssignmentSpec extends AssignmentSpec {
FieldAssignmentSpec(Assignment field) { super(field); }
protected void setValue(Event targetEvent, String value) {
try {
BeanWrapper bean = PropertyAccessorFactory.forBeanPropertyAccess(targetEvent);
bean.setPropertyValue(getAttributeName(), value);
} catch(FatalBeanException e) {
log().error("Unable to set value for attribute "+getAttributeName()+"to value "+value+ " Exception:" +e);
throw new TranslationFailedException("Unable to set value for attribute "+getAttributeName()+" to value "+value);
}
}
}
class ParameterAssignmentSpec extends AssignmentSpec {
ParameterAssignmentSpec(Assignment assign) {
super(assign);
}
protected void setValue(Event targetEvent, String value) {
if (value == null) {
log().debug("Value of parameter is null setting to blank");
value="";
}
for (final Parm parm : targetEvent.getParmCollection()) {
if (parm.getParmName().equals(getAttributeName())) {
org.opennms.netmgt.xml.event.Value val = parm.getValue();
if (val == null) {
val = new org.opennms.netmgt.xml.event.Value();
parm.setValue(val);
}
if (log().isDebugEnabled()) {
log().debug("Overriding value of parameter "+getAttributeName()+". Setting it to "+value);
}
val.setContent(value);
return;
}
}
// if we got here then we didn't find the existing parameter
Parm newParm = new Parm();
newParm.setParmName(getAttributeName());
org.opennms.netmgt.xml.event.Value val = new org.opennms.netmgt.xml.event.Value();
newParm.setValue(val);
if (log().isDebugEnabled()) {
log().debug("Setting value of parameter "+getAttributeName()+" to "+value);
}
val.setContent(value);
targetEvent.addParm(newParm);
}
}
ValueSpec getValueSpec(Value val) {
if ("field".equals(val.getType()))
return new FieldValueSpec(val);
else if ("parameter".equals(val.getType()))
return new ParameterValueSpec(val);
else if ("constant".equals(val.getType()))
return new ConstantValueSpec(val);
else if ("sql".equals(val.getType()))
return new SqlValueSpec(val);
else
return new ValueSpecUnspecified();
}
abstract class ValueSpec {
public abstract boolean matches(Event e);
public abstract String getResult(Event srcEvent);
}
class ConstantValueSpec extends ValueSpec {
Value m_constant;
public ConstantValueSpec(Value constant) {
m_constant = constant;
}
public boolean matches(Event e) {
if (m_constant.getMatches() != null) {
log().warn("ConstantValueSpec.matches: matches not allowed for constant value.");
throw new IllegalStateException("Illegal to use matches with constant type values");
}
return true;
}
public String getResult(Event srcEvent) {
return m_constant.getResult();
}
}
class ValueSpecUnspecified extends ValueSpec {
public boolean matches(Event e) {
// TODO: this should probably throw an exception since it makes no sense
return true;
}
public String getResult(Event srcEvent) {
return "value unspecified";
}
}
class SqlValueSpec extends ValueSpec {
Value m_val;
List<ValueSpec> m_nestedValues;
public SqlValueSpec(Value val) {
m_val = val;
m_nestedValues = null; // lazy init
}
public List<ValueSpec> getNestedValues() {
if (m_nestedValues == null)
m_nestedValues = constructNestedValues();
return m_nestedValues;
}
private List<ValueSpec> constructNestedValues() {
List<ValueSpec> nestedValues = new ArrayList<ValueSpec>();
for (Value val : m_val.getValueCollection()) {
nestedValues.add(EventTranslatorConfigFactory.this.getValueSpec(val));
}
return nestedValues;
}
public boolean matches(Event e) {
for (ValueSpec nestedVal : getNestedValues()) {
if (!nestedVal.matches(e))
return false;
}
Query query = createQuery(e);
int rowCount = query.execute();
if (rowCount < 1) {
log().info("No results found for query "+query.reproduceStatement()+". No match.");
return false;
}
return true;
}
private class Query {
SingleResultQuerier m_querier;
Object[] m_args;
Query(SingleResultQuerier querier, Object[] args) {
m_querier = querier;
m_args = args;
}
public int getRowCount() {
return m_querier.getCount();
}
public int execute() {
m_querier.execute(m_args);
return getRowCount();
}
public String reproduceStatement() {
return m_querier.reproduceStatement(m_args);
}
public Object getResult() {
return m_querier.getResult();
}
}
public Query createQuery(Event srcEvent) {
Object[] args = new Object[getNestedValues().size()];
SingleResultQuerier querier = new SingleResultQuerier(m_dbConnFactory, m_val.getResult());
for (int i = 0; i < args.length; i++) {
args[i] = (getNestedValues().get(i)).getResult(srcEvent);
}
return new Query(querier, args);
}
public String getResult(Event srcEvent) {
Query query = createQuery(srcEvent);
query.execute();
if (query.getRowCount() < 1) {
log().info("No results found for query "+query.reproduceStatement()+". Returning null");
return null;
}
else {
Object result = query.getResult();
if (log().isDebugEnabled()) {
log().debug("getResult: result of single result querier is:"+result);
}
if (result != null) {
return result.toString();
} else {
return null;
}
}
}
}
abstract class AttributeValueSpec extends ValueSpec {
Value m_val;
AttributeValueSpec(Value val) { m_val = val; }
public boolean matches(Event e) {
String attributeValue = getAttributeValue(e);
if (attributeValue == null) {
log().debug("AttributeValueSpec.matches: Event attributeValue doesn't match because attributeValue itself is null");
return false;
}
if (m_val.getMatches() == null) {
if (log().isDebugEnabled()) {
log().debug("AttributeValueSpec.matches: Event attributeValue: "+attributeValue+" matches because pattern is null");
}
return true;
}
Pattern p = Pattern.compile(m_val.getMatches());
Matcher m = p.matcher(attributeValue);
if (log().isDebugEnabled()) {
log().debug("AttributeValueSpec.matches: Event attributeValue: " + attributeValue + " " +
(m.matches()? "matches" : "doesn't match") + " pattern: " + m_val.getMatches());
}
if (m.matches()) {
return true;
} else {
return false;
}
}
public String getResult(Event srcEvent) {
if (m_val.getMatches() == null) return m_val.getResult();
String attributeValue = getAttributeValue(srcEvent);
if (attributeValue == null) {
throw new TranslationFailedException("failed to match null against '"+m_val.getMatches()+"' for attribute "+getAttributeName());
}
Pattern p = Pattern.compile(m_val.getMatches());
final Matcher m = p.matcher(attributeValue);
if (!m.matches())
throw new TranslationFailedException("failed to match "+attributeValue+" against '"+m_val.getMatches()+"' for attribute "+getAttributeName());
MatchTable matches = new MatchTable(m);
return PropertiesUtils.substitute(m_val.getResult(), matches);
}
public String getAttributeName() { return m_val.getName(); }
abstract public String getAttributeValue(Event e);
}
// XXX: This is here because Spring converting to a String appears
// to be broken. It if probably a Hack and we probably need to have
// a better way to access the Spring property editors and convert
// to a string more correctly.
class StringPropertyEditor extends PropertyEditorSupport {
@Override
public void setValue(Object value) {
if (value == null || value instanceof String)
super.setValue(value);
else
super.setValue(value.toString());
}
@Override
public String getAsText() {
return (String)super.getValue();
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
super.setValue(text);
}
}
class FieldValueSpec extends AttributeValueSpec {
public FieldValueSpec(Value val) {
super(val);
}
public String getAttributeValue(Event e) {
try {
BeanWrapper bean = getBeanWrapper(e);
return (String)bean.convertIfNecessary(bean.getPropertyValue(getAttributeName()), String.class);
} catch (FatalBeanException ex) {
log().error("Property "+getAttributeName()+" does not exist on Event", ex);
throw new TranslationFailedException("Property "+getAttributeName()+" does not exist on Event");
}
}
private BeanWrapper getBeanWrapper(Event e) {
BeanWrapper bean = PropertyAccessorFactory.forBeanPropertyAccess(e);
bean.registerCustomEditor(String.class, new StringPropertyEditor());
return bean;
}
}
class ParameterValueSpec extends AttributeValueSpec {
ParameterValueSpec(Value val) { super(val); }
public String getAttributeValue(Event e) {
String attrName = getAttributeName();
for (Parm parm : e.getParmCollection()) {
if (parm.getParmName().equals(attrName)) {
if (log().isDebugEnabled()) {
log().debug("getAttributeValue: eventParm name: '"+parm.getParmName()+" equals translation parameter name: '"+attrName);
}
return (parm.getValue() == null ? "" : parm.getValue().getContent());
}
String trimmedAttrName = StringUtils.removeStart(attrName, "~");
if (attrName.startsWith("~") && (parm.getParmName().matches(trimmedAttrName))) {
if (log().isDebugEnabled()) {
log().debug("getAttributeValue: eventParm name: '"+parm.getParmName()+" matches translation parameter name expression: '"+trimmedAttrName);
}
return (parm.getValue() == null ? "" : parm.getValue().getContent());
}
}
return null;
}
}
}