From 3778454e73214a1e58f26efa5b6a94826715796d Mon Sep 17 00:00:00 2001 From: Christian Pape Date: Wed, 25 Sep 2019 08:37:52 +0200 Subject: [PATCH] PRIS-151: Added support for meta-data --- docs/content/source/jdbc/_index.en.adoc | 3 + docs/content/source/xls/_index.en.adoc | 3 + .../pris/plugins/jdbc/source/JdbcSource.java | 28 +++++++++ .../plugins/jdbc/source/JdbcSourceTest.java | 25 +++++++-- .../pris/plugins/xls/source/XlsSource.java | 53 ++++++++++++++++++ .../plugins/xls/source/XlsSourceTest.java | 10 ++++ .../src/test/resources/test.xls | Bin 7168 -> 20480 bytes pom.xml | 10 +++- 8 files changed, 126 insertions(+), 6 deletions(-) diff --git a/docs/content/source/jdbc/_index.en.adoc b/docs/content/source/jdbc/_index.en.adoc index bd4bc789..2d8ec44c 100644 --- a/docs/content/source/jdbc/_index.en.adoc +++ b/docs/content/source/jdbc/_index.en.adoc @@ -34,8 +34,11 @@ The following column-headers will be mapped from the result set to the OpenNMS r | `Location` | | The monitoring location for the node. When not set, the node is monitored from the _OpenNMS_ server, otherwise from the _Minion_ associated with the `Location`. | `Cat` | | will be interpreted as a surveillance category for the node identified by the `Foreign_Id` | `Svc` | | will be interpreted as a service on the interface of the node identified by the `Foreign_Id` and `IP_Address` field +| `MetaData_` | | will be interpreted as node-level meta-data with the given key and the default context `requisition`. You can use `MetaData_Context:Key` to specify a custom context. |======================== +CAUTION: Please note, that this datasource only allows to specify node-level meta-data. + This source also supports all asset fields by using `Asset_` as a prefix followed by the `asset-field-name`. The city field of the assets can be addressed like this: `yourvalue AS Asset_City` and is not case-sensitive. diff --git a/docs/content/source/xls/_index.en.adoc b/docs/content/source/xls/_index.en.adoc index 136cd238..c94ad76f 100644 --- a/docs/content/source/xls/_index.en.adoc +++ b/docs/content/source/xls/_index.en.adoc @@ -32,8 +32,11 @@ These `column names` have to start with certain prefixes to be recognized. | `InterfaceStatus` | | will be interpreted as interface status. Use `1` for monitored and `3` for not monitored. | `cat_` | | will be interpreted as a surveillance category. Multiple comma-separated categories can be provided. It can be used multiple times per sheet. | `svc_` | | will be interpreted as a service on the interface of the node. Multiple comma-separated services can be provided. It can be used multiple times per sheet. +| `MetaData_` | | will be interpreted as node-level meta-data with the given key and the default context `requisition`. You can use `MetaData_Context:Key` to specify a custom context. |======================== +CAUTION: Please note, that this datasource only allows to specify node-level meta-data. + This source also supports all asset-fields by using `Asset_` as a prefix followed by the `asset-field-name`. The city field of the assets can be addressed like this: `Asset_City`. This is not case-sensitive. diff --git a/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/main/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSource.java b/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/main/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSource.java index 8327fc05..8f541fbb 100644 --- a/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/main/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSource.java +++ b/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/main/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSource.java @@ -22,6 +22,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; @@ -29,6 +30,7 @@ import org.opennms.pris.api.InstanceConfiguration; import org.opennms.pris.api.Source; import org.opennms.pris.model.AssetField; +import org.opennms.pris.model.MetaData; import org.opennms.pris.model.PrimaryType; import org.opennms.pris.model.Requisition; import org.opennms.pris.model.RequisitionAsset; @@ -40,6 +42,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Strings; + /** * A JDBC data source allows to connect to an SQL database and extract data in a given format. The result set is mapped to an OpenNMS requisition. */ @@ -65,6 +69,7 @@ public class JdbcSource implements Source { private static final String COLUMN_PARENT_NODE_LABEL = "Parent_Node_Label"; private static final String COLUMN_PARENT_FOREIGN_ID = "Parent_Foreign_Id"; private static final String COLUMN_PARENT_FOREIGN_SOURCE = "Parent_Foreign_Source"; + private static final String COLUMN_METADATA_PREFIX = "MetaData_"; public JdbcSource(final InstanceConfiguration config) { this.config = config; @@ -207,6 +212,29 @@ public Object dump() { asset.setValue(assetValue); } } + + final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + + for (int i = 0; i < resultSetMetaData.getColumnCount(); i++) { + final String columnName = resultSetMetaData.getColumnLabel(i + 1); + + if (columnName.toLowerCase().startsWith(COLUMN_METADATA_PREFIX.toLowerCase())) { + final String value = getString(resultSet, columnName); + + if (!Strings.isNullOrEmpty(value)) { + String context = "requisition"; + String key = columnName.substring(COLUMN_METADATA_PREFIX.length()); + final int index = key.indexOf(":"); + + if (index != -1) { + context = key.substring(0, index); + key = key.substring(index + 1); + } + + node.getMetaDatas().add(new MetaData(context, key, value)); + } + } + } } } else { LOGGER.error("ResultSet is null"); diff --git a/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/test/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSourceTest.java b/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/test/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSourceTest.java index 690c477d..16b8ca08 100644 --- a/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/test/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSourceTest.java +++ b/opennms-pris-plugins/opennms-pris-plugins-jdbc/src/test/java/org/opennms/opennms/pris/plugins/jdbc/source/JdbcSourceTest.java @@ -6,13 +6,16 @@ import java.sql.SQLException; import java.sql.Statement; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.opennms.pris.api.MockInstanceConfiguration; +import org.opennms.pris.model.MetaData; import org.opennms.pris.model.Requisition; +import org.opennms.pris.model.RequisitionNode; public class JdbcSourceTest { @@ -38,6 +41,8 @@ public class JdbcSourceTest { + "state VARCHAR(255)," + "serviceName VARCHAR(255)," + "categoryName VARCHAR(255)," + + "metaDataColumn1 VARCHAR(255)," + + "metaDataColumn2 VARCHAR(255)," + "PRIMARY KEY (foreignId))"; private final String SQL_SELECT_STATEMENT_TEST_1 = "SELECT foreignId AS Foreign_Id, " @@ -55,6 +60,8 @@ public class JdbcSourceTest { + "city AS Asset_City," + "state AS Asset_State," + "serviceName AS Svc," + + "metaDataColumn1 AS \"MetaData_keyWithoutContext\"," + + "metaDataColumn2 AS \"MetaData_Context:keyWithContext\"," + "categoryName AS Cat FROM node"; @BeforeClass @@ -69,13 +76,13 @@ public void setUp() throws ClassNotFoundException, SQLException { Statement stmnt = connection.createStatement(); stmnt.executeUpdate(SQL_CREATE_ALL); - insertRow("1", "node1", null, null, null, null, "192.168.0.1", "P", "1", "description1", "city1", "state1", "service1", "category1"); - insertRow("2", "node2", "Test-Location", "ParentNodeLabel", "", "", "192.168.0.2", "P", "3", "description2", "city2", "state2", "service2", "category2"); - insertRow("3", "node3", null, null, "ParentId", "ParentSouce", "192.168.0.3", "P", "1", "description3", "city3", "state3", "service3", "category3"); + insertRow("1", "node1", null, null, null, null, "192.168.0.1", "P", "1", "description1", "city1", "state1", "service1", "category1", "Foo1", "Bar1"); + insertRow("2", "node2", "Test-Location", "ParentNodeLabel", "", "", "192.168.0.2", "P", "3", "description2", "city2", "state2", "service2", "category2", "Foo2", "Bar2"); + insertRow("3", "node3", null, null, "ParentId", "ParentSouce", "192.168.0.3", "P", "1", "description3", "city3", "state3", "service3", "category3", "Foo3", "Bar3"); } - private void insertRow(String foreignId, String nodeLabel, String location, String parentNodeLabel, String parentForeignId, String parentForeignSource, String ipAddress, String ifType, String ifStatus, String description, String city, String state, String serviceName, String categoryName) throws SQLException { - String DML = "INSERT INTO node (foreignId, nodeLabel, location, parentNodeLabel, parentForeignId, parentForeignSource, ipAddress, ifType, ifStatus, description, city, state, serviceName, categoryName) VALUES (" + foreignId + ", '" + nodeLabel + "', '" + location + "', '" + parentNodeLabel + "', '" + parentForeignId + "', '" + parentForeignSource + "', '" + ipAddress + "', '" + ifType + "', '" + ifStatus + "', '" + description + "', '" + city + "', '" + state + "', '" + serviceName + "', '" + categoryName + "')"; + private void insertRow(String foreignId, String nodeLabel, String location, String parentNodeLabel, String parentForeignId, String parentForeignSource, String ipAddress, String ifType, String ifStatus, String description, String city, String state, String serviceName, String categoryName, String metaDataColumn1, String metaDataColumn2) throws SQLException { + String DML = "INSERT INTO node (foreignId, nodeLabel, location, parentNodeLabel, parentForeignId, parentForeignSource, ipAddress, ifType, ifStatus, description, city, state, serviceName, categoryName, metaDataColumn1, metaDataColumn2) VALUES (" + foreignId + ", '" + nodeLabel + "', '" + location + "', '" + parentNodeLabel + "', '" + parentForeignId + "', '" + parentForeignSource + "', '" + ipAddress + "', '" + ifType + "', '" + ifStatus + "', '" + description + "', '" + city + "', '" + state + "', '" + serviceName + "', '" + categoryName + "', '" + metaDataColumn1 +"', '" + metaDataColumn2 +"')"; Statement stmnt = connection.createStatement(); stmnt.executeUpdate(DML); } @@ -115,5 +122,13 @@ public void testDump() { Assert.assertEquals(3, requisition.getNodes().size()); Assert.assertEquals("Test-Location", requisition.getNodes().get(1).getLocation()); Assert.assertEquals("ParentNodeLabel", requisition.getNodes().get(1).getParentNodeLabel()); + + final RequisitionNode node1 = requisition.getNodes().stream().filter(n -> n.getForeignId().equals("1")).findFirst().get(); + final RequisitionNode node2 = requisition.getNodes().stream().filter(n -> n.getForeignId().equals("2")).findFirst().get(); + final RequisitionNode node3 = requisition.getNodes().stream().filter(n -> n.getForeignId().equals("3")).findFirst().get(); + + Assert.assertThat(node1.getMetaDatas(), Matchers.containsInAnyOrder(new MetaData("requisition", "keyWithoutContext", "Foo1"), new MetaData("Context", "keyWithContext", "Bar1"))); + Assert.assertThat(node2.getMetaDatas(), Matchers.containsInAnyOrder(new MetaData("requisition", "keyWithoutContext", "Foo2"), new MetaData("Context", "keyWithContext", "Bar2"))); + Assert.assertThat(node3.getMetaDatas(), Matchers.containsInAnyOrder(new MetaData("requisition", "keyWithoutContext", "Foo3"), new MetaData("Context", "keyWithContext", "Bar3"))); } } \ No newline at end of file diff --git a/opennms-pris-plugins/opennms-pris-plugins-xls/src/main/java/org/opennms/opennms/pris/plugins/xls/source/XlsSource.java b/opennms-pris-plugins/opennms-pris-plugins-xls/src/main/java/org/opennms/opennms/pris/plugins/xls/source/XlsSource.java index f3625660..b9f2cf24 100644 --- a/opennms-pris-plugins/opennms-pris-plugins-xls/src/main/java/org/opennms/opennms/pris/plugins/xls/source/XlsSource.java +++ b/opennms-pris-plugins/opennms-pris-plugins-xls/src/main/java/org/opennms/opennms/pris/plugins/xls/source/XlsSource.java @@ -42,6 +42,7 @@ import org.opennms.pris.api.InstanceConfiguration; import org.opennms.pris.api.Source; import org.opennms.pris.model.AssetField; +import org.opennms.pris.model.MetaData; import org.opennms.pris.model.PrimaryType; import org.opennms.pris.model.Requisition; import org.opennms.pris.model.RequisitionAsset; @@ -52,6 +53,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Strings; + public class XlsSource implements Source { private static final Logger LOGGER = LoggerFactory @@ -62,6 +65,8 @@ public class XlsSource implements Source { private final String WITHIN_SPLITTER = ","; private final String PREFIX_FOR_ASSETS = "Asset_"; + private final String PREFIX_FOR_METADATA = "MetaData_"; + private final String INTERFACE_TYPE_PRIMARY = "P"; private final String INTERFACE_TYPE_SECONDARY = "S"; @@ -69,6 +74,8 @@ public class XlsSource implements Source { private Map> optionalMultiColumns; private Map optionalUniquHeaders; private Map assetColumns; + private Map> metaDataColumns; + private File xls; private final String encoding; @@ -159,6 +166,7 @@ public Object dump() throws MissingRequiredColumnHeaderException, Exception { optionalMultiColumns = initializeOptionalMultiColumns(sheet); optionalUniquHeaders = initializeOptionalUniquHeaders(sheet); assetColumns = initializeAssetColumns(sheet); + metaDataColumns = initializeMetaDataColumns(sheet); RequisitionInterface reqInterface; Iterator rowiterator = sheet.rowIterator(); @@ -214,6 +222,9 @@ public Object dump() throws MissingRequiredColumnHeaderException, Exception { // adding assets node.getAssets().addAll(getAssetsByRow(row)); + // adding meta-data + node.getMetaDatas().addAll(getMetaDataByRow(row)); + // Add interface reqInterface = getInterfaceByRow(row); @@ -305,6 +316,24 @@ private Map> initializeOptionalMultiColumns( return result; } + private Map> initializeMetaDataColumns(final Sheet sheet) { + final Map> result = new HashMap<>(); + final Row row = sheet.getRow(0); + final Iterator celliterator = row.cellIterator(); + while (celliterator.hasNext()) { + Cell cell = celliterator.next(); + final String cellName = cell.getStringCellValue(); + if (cellName.toLowerCase().startsWith(PREFIX_FOR_METADATA.toLowerCase())) { + final String metaDataKey = cellName.substring(PREFIX_FOR_METADATA.length()); + if (!result.containsKey(metaDataKey)) { + result.put(metaDataKey, new ArrayList<>()); + } + result.get(metaDataKey).add(cell.getColumnIndex()); + } + } + return result; + } + private Map initializeAssetColumns(Sheet sheet) { Map result = new HashMap<>(); for (AssetField prefix : AssetField.values()) { @@ -392,6 +421,30 @@ private Set getAssetsByRow(Row row) { return assets; } + private Set getMetaDataByRow(Row row) { + Set metaData = new HashSet<>(); + for (final Map.Entry> entry : metaDataColumns.entrySet()) { + for (final int columnIndex : entry.getValue()) { + final Cell cell = row.getCell(columnIndex); + if (cell == null) { + continue; + } + final String value = XlsSource.getStringValueFromCell(cell); + if (!Strings.isNullOrEmpty(value)) { + String context = "requisition"; + String key = entry.getKey(); + final int index = entry.getKey().indexOf(":"); + if (index != -1) { + context = entry.getKey().substring(0, index); + key = entry.getKey().substring(index + 1); + } + metaData.add(new MetaData(context, key, value)); + } + } + } + return metaData; + } + private RequisitionInterface getInterfaceByRow(Row row) throws InvalidInterfaceException { RequisitionInterface reqInterface = new RequisitionInterface(); diff --git a/opennms-pris-plugins/opennms-pris-plugins-xls/src/test/java/org/opennms/opennms/pris/plugins/xls/source/XlsSourceTest.java b/opennms-pris-plugins/opennms-pris-plugins-xls/src/test/java/org/opennms/opennms/pris/plugins/xls/source/XlsSourceTest.java index bb367e84..25e917a1 100644 --- a/opennms-pris-plugins/opennms-pris-plugins-xls/src/test/java/org/opennms/opennms/pris/plugins/xls/source/XlsSourceTest.java +++ b/opennms-pris-plugins/opennms-pris-plugins-xls/src/test/java/org/opennms/opennms/pris/plugins/xls/source/XlsSourceTest.java @@ -1,11 +1,17 @@ package org.opennms.opennms.pris.plugins.xls.source; import java.nio.file.Paths; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + import org.junit.Before; import org.junit.Test; import org.opennms.pris.api.MockInstanceConfiguration; import org.opennms.pris.model.AssetField; +import org.opennms.pris.model.MetaData; import org.opennms.pris.model.PrimaryType; import org.opennms.pris.model.Requisition; import org.opennms.pris.model.RequisitionCategory; @@ -58,6 +64,10 @@ public void basicTest() throws Exception { RequisitionCategory findCategory = RequisitionUtils.findCategory(resultNode, "Test"); assertEquals("Test", findCategory.getName()); + + assertThat(resultNode.getMetaDatas(), containsInAnyOrder( + new MetaData("requisition", "KeyWithoutContext", "Foo"), + new MetaData("Context", "KeyWithContext", "Bar"))); } @Test diff --git a/opennms-pris-plugins/opennms-pris-plugins-xls/src/test/resources/test.xls b/opennms-pris-plugins/opennms-pris-plugins-xls/src/test/resources/test.xls index 5a57f8bfaab5e28ca3015bbe10e6e19106a9ae9e..7ca289fd0a3e3bbb86be9201ecad7946a56145ad 100644 GIT binary patch literal 20480 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.6 2.4.15 27.0-jre + 2.1 4.5.6 9.4.12.v20180830 4.12 @@ -40,7 +41,7 @@ 3.8.0 1.8 8.0.13 - 22.0.4 + 24.1.3 42.2.5 1.7.25 interval:60 @@ -77,6 +78,13 @@ ${junitVersion} test + + + org.hamcrest + hamcrest + ${hamcrestVersion} + test +