From d89c8c243227c579b137de90784cfdd879ddc733 Mon Sep 17 00:00:00 2001 From: JSimo Date: Wed, 29 Apr 2026 16:29:17 +0200 Subject: [PATCH] Use compact wire marshalling for IPC and embedded payloads Add MarshallerProfile (WIRE/CONFIG) with unified marshaller setup and per-thread caching. RPC, sink, and tracing use the wire profile; config file embeds keep CONFIG marshal-time XSD validation. Split poller embedded marshalling by context: PollerWireClassObjectAdapter for RPC attributes (compact, no marshal XSD) and PollerClassObjectAdapter for poller-configuration.xml parameters. Marshal embedded payloads via DOMResult instead of marshal-to-string plus DocumentBuilder.parse. Add unit tests for wire/config profiles, poller adapter behavior, and RPC wire round-trips. --- .../core/rpc/xml/AbstractXmlRpcModule.java | 4 +- .../ipc/sink/xml/AbstractXmlSinkModule.java | 4 +- .../core/tracing/util/TracingInfoCarrier.java | 2 +- .../core/xml/JaxbClassObjectAdapter.java | 25 ++- .../java/org/opennms/core/xml/JaxbUtils.java | 146 ++++++++++++++--- .../java/org/opennms/core/xml/XmlHandler.java | 14 +- .../core/xml/JaxbUtilsWireMarshalTest.java | 153 ++++++++++++++++++ .../poller/client/rpc/PollerAttributeDTO.java | 4 +- .../rpc/PollerRequestWireRoundTripTest.java | 77 +++++++++ .../poller/PollerWireClassObjectAdapter.java | 36 +++++ .../PollerClassObjectAdapterProfileTest.java | 140 ++++++++++++++++ 11 files changed, 563 insertions(+), 42 deletions(-) create mode 100644 core/xml/src/test/java/org/opennms/core/xml/JaxbUtilsWireMarshalTest.java create mode 100644 features/poller/client-rpc/src/test/java/org/opennms/netmgt/poller/client/rpc/PollerRequestWireRoundTripTest.java create mode 100644 opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/poller/PollerWireClassObjectAdapter.java create mode 100644 opennms-config-jaxb/src/test/java/org/opennms/netmgt/config/poller/PollerClassObjectAdapterProfileTest.java diff --git a/core/ipc/rpc/xml/src/main/java/org/opennms/core/rpc/xml/AbstractXmlRpcModule.java b/core/ipc/rpc/xml/src/main/java/org/opennms/core/rpc/xml/AbstractXmlRpcModule.java index 08e6e37a442b..119d46653c28 100644 --- a/core/ipc/rpc/xml/src/main/java/org/opennms/core/rpc/xml/AbstractXmlRpcModule.java +++ b/core/ipc/rpc/xml/src/main/java/org/opennms/core/rpc/xml/AbstractXmlRpcModule.java @@ -94,14 +94,14 @@ private XmlHandler getResponseXmlHandler() { private XmlHandler createXmlHandler(Class clazz) { try { - return new XmlHandler<>(clazz); + return XmlHandler.forWire(clazz); } catch (Throwable t) { // NMS-8793: This is a work-around for some failure in the Minion container // When invoked for the first time, the creation may fail due to // errors of the form "invalid protocol handler: mvn", but subsequent // calls always seem to work LOG.warn("Creating the XmlHandler failed. Retrying.", t); - return new XmlHandler<>(clazz); + return XmlHandler.forWire(clazz); } } } diff --git a/core/ipc/sink/xml/src/main/java/org/opennms/core/ipc/sink/xml/AbstractXmlSinkModule.java b/core/ipc/sink/xml/src/main/java/org/opennms/core/ipc/sink/xml/AbstractXmlSinkModule.java index a9cc61e1ea90..5e928529e5cb 100644 --- a/core/ipc/sink/xml/src/main/java/org/opennms/core/ipc/sink/xml/AbstractXmlSinkModule.java +++ b/core/ipc/sink/xml/src/main/java/org/opennms/core/ipc/sink/xml/AbstractXmlSinkModule.java @@ -92,14 +92,14 @@ private XmlHandler getXmlHandler() { private XmlHandler createXmlHandler(Class clazz) { try { - return new XmlHandler<>(clazz); + return XmlHandler.forWire(clazz); } catch (Throwable t) { // NMS-8793: This is a work-around for some failure in the Minion container // When invoked for the first time, the creation may fail due to // errors of the form "invalid protocol handler: mvn", but subsequent // calls always seem to work LOG.warn("Creating the XmlHandler failed. Retrying.", t); - return new XmlHandler<>(clazz); + return XmlHandler.forWire(clazz); } } } diff --git a/core/tracing/api/src/main/java/org/opennms/core/tracing/util/TracingInfoCarrier.java b/core/tracing/api/src/main/java/org/opennms/core/tracing/util/TracingInfoCarrier.java index 99eaa8368dcf..2aa920ffb865 100644 --- a/core/tracing/api/src/main/java/org/opennms/core/tracing/util/TracingInfoCarrier.java +++ b/core/tracing/api/src/main/java/org/opennms/core/tracing/util/TracingInfoCarrier.java @@ -145,7 +145,7 @@ public static TracingInfoCarrier unmarshalRequest(String tracingInfo) { private static XmlHandler createXmlHandler() { XmlHandler xmlHandler = xmlHandlerThreadLocal.get(); if (xmlHandler == null) { - xmlHandler = new XmlHandler<>(TracingInfoCarrier.class); + xmlHandler = XmlHandler.forWire(TracingInfoCarrier.class); xmlHandlerThreadLocal.set(xmlHandler); } return xmlHandler; diff --git a/core/xml/src/main/java/org/opennms/core/xml/JaxbClassObjectAdapter.java b/core/xml/src/main/java/org/opennms/core/xml/JaxbClassObjectAdapter.java index 2cbc37ed6b50..5bdb2cc0859d 100644 --- a/core/xml/src/main/java/org/opennms/core/xml/JaxbClassObjectAdapter.java +++ b/core/xml/src/main/java/org/opennms/core/xml/JaxbClassObjectAdapter.java @@ -21,18 +21,15 @@ */ package org.opennms.core.xml; -import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Map; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlAdapter; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSSerializer; @@ -91,10 +88,7 @@ public Object marshal(final Object from) throws Exception { if (from == null) return null; try { - final String s = JaxbUtils.marshal(from); - final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - final Document doc = builder.parse(new ByteArrayInputStream(s.getBytes())); - final Node node = doc.getDocumentElement(); + final Node node = marshalToDomElement(from); LOG.trace("marshal: node = {}", node); return node; } catch (final Exception e) { @@ -104,6 +98,21 @@ public Object marshal(final Object from) throws Exception { } } + protected Element marshalToDomElement(final Object from) { + return JaxbUtils.marshalToDomElement(from, marshalProfile()); + } + + /** + * Selects marshal-time formatting and XSD validation for embedded JAXB objects. + * + *

Override to return {@link JaxbUtils.MarshallerProfile#WIRE} for RPC/sink payloads. + * The default {@link JaxbUtils.MarshallerProfile#CONFIG} validates against XSD when + * available and is appropriate for config persisted to disk (e.g. poller parameters).

+ */ + protected JaxbUtils.MarshallerProfile marshalProfile() { + return JaxbUtils.MarshallerProfile.CONFIG; + } + public Class getClassForElement(String nodeName) { final Class clazz = m_knownElementClasses.get(nodeName.toLowerCase()); if (clazz != null) { diff --git a/core/xml/src/main/java/org/opennms/core/xml/JaxbUtils.java b/core/xml/src/main/java/org/opennms/core/xml/JaxbUtils.java index 6b8d7096774b..287607c79238 100644 --- a/core/xml/src/main/java/org/opennms/core/xml/JaxbUtils.java +++ b/core/xml/src/main/java/org/opennms/core/xml/JaxbUtils.java @@ -39,6 +39,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -57,8 +58,12 @@ import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.stream.FactoryConfigurationError; import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMResult; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; @@ -74,11 +79,26 @@ import org.xml.sax.SAXException; import org.xml.sax.XMLFilter; import org.xml.sax.XMLReader; +import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.xml.sax.helpers.XMLReaderFactory; public abstract class JaxbUtils { private static final Logger LOG = LoggerFactory.getLogger(JaxbUtils.class); + public enum MarshallerProfile { + /** + * Compact output without marshal-time XSD validation. Use for RPC, sink, tracing, + * and other in-process wire payloads where objects are already constructed in Java. + */ + WIRE, + /** + * Pretty-printed output with marshal-time XSD validation when an XSD is available. + * Use for config files and other human-oriented XML persisted to disk. + */ + CONFIG + } + private static final Class[] EMPTY_CLASS_LIST = new Class[0]; private static final Source[] EMPTY_SOURCE_LIST = new Source[0]; @@ -95,8 +115,9 @@ public boolean handleEvent(final ValidationEvent event) { } private static final MarshallingExceptionTranslator EXCEPTION_TRANSLATOR = new MarshallingExceptionTranslator(); - private static ThreadLocal, Marshaller>> m_marshallers = new ThreadLocal, Marshaller>>(); + private static ThreadLocal, Map>> m_marshallers = new ThreadLocal<>(); private static ThreadLocal, Unmarshaller>> m_unMarshallers = new ThreadLocal, Unmarshaller>>(); + private static final ThreadLocal m_threadLocalDocumentBuilder = new ThreadLocal<>(); private static final Map,JAXBContext> m_contexts = Collections.synchronizedMap(new WeakHashMap,JAXBContext>()); private static final Map,Schema> m_schemas = Collections.synchronizedMap(new WeakHashMap,Schema>()); private static final Map> m_elementClasses = Collections.synchronizedMap(new WeakHashMap>()); @@ -321,47 +342,120 @@ public static XMLFilter getXMLFilterForNamespace(final String namespace) throws return filter; } + /** + * Marshaller for machine-oriented XML (RPC/sink payloads): no pretty-print, no schema validation. + */ + public static Marshaller createWireMarshaller(final JAXBContext context) throws JAXBException { + return createMarshaller(context, null, MarshallerProfile.WIRE); + } + + /** + * Marshal a JAXB root object to a DOM element using {@link MarshallerProfile#WIRE}. + * + *

Package-private so callers outside {@code org.opennms.core.xml} must pass an explicit + * {@link MarshallerProfile}. Config-file embeds should use + * {@link #marshalToDomElement(Object, MarshallerProfile)} with {@link MarshallerProfile#CONFIG} + * or {@link JaxbClassObjectAdapter}.

+ */ + static Element marshalToDomElement(final Object obj) { + return marshalToDomElement(obj, MarshallerProfile.WIRE); + } + + /** + * Marshal a JAXB root object to a DOM element for embedding without marshal-to-string + + * {@link DocumentBuilder#parse}. The profile selects compact wire output or validated config output. + */ + public static Element marshalToDomElement(final Object obj, final MarshallerProfile profile) { + try { + final Class clazz = obj.getClass(); + final Marshaller marshaller = getMarshallerFor(clazz, null, profile); + final Document doc = getThreadLocalDocumentBuilder().newDocument(); + final DOMResult domResult = new DOMResult(doc); + marshaller.marshal(obj, domResult); + final Element root = doc.getDocumentElement(); + if (root == null) { + throw new IllegalStateException("JAXB produced no document element for " + clazz.getName()); + } + return root; + } catch (final JAXBException e) { + throw EXCEPTION_TRANSLATOR.translate("marshalling embedded " + obj.getClass().getSimpleName(), e); + } + } + + private static DocumentBuilder getThreadLocalDocumentBuilder() { + DocumentBuilder builder = m_threadLocalDocumentBuilder.get(); + if (builder != null) { + return builder; + } + try { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + builder = dbf.newDocumentBuilder(); + } catch (final ParserConfigurationException e) { + throw EXCEPTION_TRANSLATOR.translate("creating DocumentBuilder for embedded JAXB marshal", e); + } + m_threadLocalDocumentBuilder.set(builder); + return builder; + } + public static Marshaller getMarshallerFor(final Object obj, final JAXBContext jaxbContext) { + return getMarshallerFor(obj, jaxbContext, MarshallerProfile.CONFIG); + } + + public static Marshaller getMarshallerFor(final Object obj, final JAXBContext jaxbContext, final MarshallerProfile profile) { final Class clazz = (Class)(obj instanceof Class ? obj : obj.getClass()); - Map, Marshaller> marshallers = m_marshallers.get(); if (jaxbContext == null) { + Map, Map> marshallers = m_marshallers.get(); if (marshallers == null) { - marshallers = new WeakHashMap, Marshaller>(); + marshallers = new WeakHashMap<>(); m_marshallers.set(marshallers); } - if (marshallers.containsKey(clazz)) { - LOG.trace("found unmarshaller for {}", clazz); - return marshallers.get(clazz); + final Map byProfile = marshallers.get(clazz); + if (byProfile != null && byProfile.containsKey(profile)) { + LOG.trace("found marshaller for {} ({})", clazz, profile); + return byProfile.get(profile); + } + LOG.trace("creating marshaller for {} ({})", clazz, profile); + try { + final Marshaller marshaller = createMarshaller(getContextFor(clazz), clazz, profile); + Map profileMap = byProfile; + if (profileMap == null) { + profileMap = new EnumMap<>(MarshallerProfile.class); + marshallers.put(clazz, profileMap); + } + profileMap.put(profile, marshaller); + return marshaller; + } catch (final JAXBException e) { + throw EXCEPTION_TRANSLATOR.translate("creating XML marshaller", e); } } - LOG.trace("creating unmarshaller for {}", clazz); + LOG.trace("creating marshaller for {} ({}) with explicit context", clazz, profile); try { - final JAXBContext context; - if (jaxbContext == null) { - context = getContextFor(clazz); - } else { - context = jaxbContext; - } - final Marshaller marshaller = context.createMarshaller(); - marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); - marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); - if (context.getClass().getName().startsWith("org.eclipse.persistence.jaxb")) { - marshaller.setProperty(MarshallerProperties.NAMESPACE_PREFIX_MAPPER, new EmptyNamespacePrefixMapper()); - marshaller.setProperty(MarshallerProperties.JSON_MARSHAL_EMPTY_COLLECTIONS, true); - } - final Schema schema = getValidatorFor(clazz); - marshaller.setSchema(schema); - if (jaxbContext == null) marshallers.put(clazz, marshaller); - - return marshaller; + return createMarshaller(jaxbContext, clazz, profile); } catch (final JAXBException e) { throw EXCEPTION_TRANSLATOR.translate("creating XML marshaller", e); } } + private static Marshaller createMarshaller(final JAXBContext context, final Class clazz, final MarshallerProfile profile) throws JAXBException { + final Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, profile == MarshallerProfile.CONFIG); + marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); + if (context.getClass().getName().startsWith("org.eclipse.persistence.jaxb")) { + marshaller.setProperty(MarshallerProperties.NAMESPACE_PREFIX_MAPPER, new EmptyNamespacePrefixMapper()); + marshaller.setProperty(MarshallerProperties.JSON_MARSHAL_EMPTY_COLLECTIONS, Boolean.TRUE); + } + if (profile == MarshallerProfile.WIRE) { + marshaller.setSchema(null); + } else { + marshaller.setSchema(getValidatorFor(clazz)); + } + return marshaller; + } + /** * Get a JAXB unmarshaller for the given object. If no JAXBContext is provided, * JAXBUtils will create and cache a context for the given object. diff --git a/core/xml/src/main/java/org/opennms/core/xml/XmlHandler.java b/core/xml/src/main/java/org/opennms/core/xml/XmlHandler.java index bf02ee96aa97..0bec80348be0 100644 --- a/core/xml/src/main/java/org/opennms/core/xml/XmlHandler.java +++ b/core/xml/src/main/java/org/opennms/core/xml/XmlHandler.java @@ -45,7 +45,19 @@ public class XmlHandler { private final Marshaller marshaller; private final Unmarshaller unmarshaller; + public static XmlHandler forWire(Class clazz) { + return new XmlHandler<>(clazz, JaxbUtils.MarshallerProfile.WIRE); + } + + public static XmlHandler forConfig(Class clazz) { + return new XmlHandler<>(clazz, JaxbUtils.MarshallerProfile.CONFIG); + } + public XmlHandler(Class clazz) { + this(clazz, JaxbUtils.MarshallerProfile.CONFIG); + } + + private XmlHandler(Class clazz, JaxbUtils.MarshallerProfile profile) { this.clazz = clazz; JAXBContext context; try { @@ -53,7 +65,7 @@ public XmlHandler(Class clazz) { } catch (JAXBException e) { throw new RuntimeException(e); } - this.marshaller = JaxbUtils.getMarshallerFor(clazz, context); + this.marshaller = JaxbUtils.getMarshallerFor(clazz, context, profile); this.unmarshaller = JaxbUtils.getUnmarshallerFor(clazz, context, false); // Use the same event handler that we use in JaxbUtils try { diff --git a/core/xml/src/test/java/org/opennms/core/xml/JaxbUtilsWireMarshalTest.java b/core/xml/src/test/java/org/opennms/core/xml/JaxbUtilsWireMarshalTest.java new file mode 100644 index 000000000000..f97838cb3211 --- /dev/null +++ b/core/xml/src/test/java/org/opennms/core/xml/JaxbUtilsWireMarshalTest.java @@ -0,0 +1,153 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.core.xml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.StringWriter; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.Test; +import org.opennms.core.xml.JaxbUtils.MarshallerProfile; +import org.w3c.dom.Element; + +public class JaxbUtilsWireMarshalTest { + + @XmlRootElement(name = "wire-test") + public static class WireTestBean { + private String value; + private int count; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + } + + private static WireTestBean sampleBean() { + final WireTestBean bean = new WireTestBean(); + bean.setValue("hello"); + bean.setCount(42); + return bean; + } + + @Test + public void wireMarshallerIsMoreCompactThanConfig() throws Exception { + final WireTestBean bean = sampleBean(); + final JAXBContext context = JaxbUtils.getContextFor(WireTestBean.class); + + final StringWriter wireWriter = new StringWriter(); + JaxbUtils.createWireMarshaller(context).marshal(bean, wireWriter); + final String wireXml = wireWriter.toString(); + + final StringWriter configWriter = new StringWriter(); + JaxbUtils.getMarshallerFor(WireTestBean.class, context, MarshallerProfile.CONFIG).marshal(bean, configWriter); + final String configXml = configWriter.toString(); + + assertFalse(wireXml.contains("\n ")); + assertTrue(configXml.length() >= wireXml.length()); + } + + @Test + public void marshalToDomElementProducesRootElement() { + final Element element = JaxbUtils.marshalToDomElement(sampleBean(), MarshallerProfile.WIRE); + assertNotNull(element); + assertEquals("wire-test", element.getLocalName()); + } + + @Test + public void marshalToDomElementWithConfigProducesRootElement() { + final Element element = JaxbUtils.marshalToDomElement(sampleBean(), MarshallerProfile.CONFIG); + assertNotNull(element); + assertEquals("wire-test", element.getLocalName()); + } + + @Test + public void wireAndConfigMarshalProduceSemanticallyEqualObjects() throws Exception { + final WireTestBean bean = sampleBean(); + final JAXBContext context = JaxbUtils.getContextFor(WireTestBean.class); + + final StringWriter wireWriter = new StringWriter(); + JaxbUtils.getMarshallerFor(WireTestBean.class, context, MarshallerProfile.WIRE).marshal(bean, wireWriter); + + final StringWriter configWriter = new StringWriter(); + JaxbUtils.getMarshallerFor(WireTestBean.class, context, MarshallerProfile.CONFIG).marshal(bean, configWriter); + + final WireTestBean fromWire = JaxbUtils.unmarshal(WireTestBean.class, wireWriter.toString(), false); + final WireTestBean fromConfig = JaxbUtils.unmarshal(WireTestBean.class, configWriter.toString(), false); + assertEquals(fromWire.getValue(), fromConfig.getValue()); + assertEquals(fromWire.getCount(), fromConfig.getCount()); + } + + @Test + public void jaxbClassObjectAdapterRoundTrip() throws Exception { + final WireTestBean original = sampleBean(); + final JaxbClassObjectAdapter adapter = new JaxbClassObjectAdapter(WireTestBean.class); + + final Object marshalled = adapter.marshal(original); + assertTrue(marshalled instanceof Element); + + final Object unmarshalled = adapter.unmarshal(marshalled); + assertTrue(unmarshalled instanceof WireTestBean); + final WireTestBean restored = (WireTestBean) unmarshalled; + assertEquals(original.getValue(), restored.getValue()); + assertEquals(original.getCount(), restored.getCount()); + } + + @Test + public void xmlHandlerForWireRoundTrip() { + final WireTestBean original = sampleBean(); + final XmlHandler handler = XmlHandler.forWire(WireTestBean.class); + + final String xml = handler.marshal(original); + assertNotNull(xml); + assertFalse(xml.isEmpty()); + + final WireTestBean restored = handler.unmarshal(xml); + assertEquals(original.getValue(), restored.getValue()); + assertEquals(original.getCount(), restored.getCount()); + } + + @Test + public void wireMarshallerIsCachedPerThread() throws Exception { + final Marshaller first = JaxbUtils.getMarshallerFor(WireTestBean.class, null, MarshallerProfile.WIRE); + final Marshaller second = JaxbUtils.getMarshallerFor(WireTestBean.class, null, MarshallerProfile.WIRE); + assertEquals(first, second); + } +} diff --git a/features/poller/client-rpc/src/main/java/org/opennms/netmgt/poller/client/rpc/PollerAttributeDTO.java b/features/poller/client-rpc/src/main/java/org/opennms/netmgt/poller/client/rpc/PollerAttributeDTO.java index 17602c70cb63..6bd7a4977708 100644 --- a/features/poller/client-rpc/src/main/java/org/opennms/netmgt/poller/client/rpc/PollerAttributeDTO.java +++ b/features/poller/client-rpc/src/main/java/org/opennms/netmgt/poller/client/rpc/PollerAttributeDTO.java @@ -30,7 +30,7 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; -import org.opennms.netmgt.config.poller.PollerClassObjectAdapter; +import org.opennms.netmgt.config.poller.PollerWireClassObjectAdapter; @XmlRootElement(name = "attribute") @XmlAccessorType(XmlAccessType.NONE) @@ -43,7 +43,7 @@ public class PollerAttributeDTO { private String value; @XmlAnyElement(lax=false) - @XmlJavaTypeAdapter(PollerClassObjectAdapter.class) + @XmlJavaTypeAdapter(PollerWireClassObjectAdapter.class) private Object contents; public PollerAttributeDTO() { diff --git a/features/poller/client-rpc/src/test/java/org/opennms/netmgt/poller/client/rpc/PollerRequestWireRoundTripTest.java b/features/poller/client-rpc/src/test/java/org/opennms/netmgt/poller/client/rpc/PollerRequestWireRoundTripTest.java new file mode 100644 index 000000000000..e0728fa43221 --- /dev/null +++ b/features/poller/client-rpc/src/test/java/org/opennms/netmgt/poller/client/rpc/PollerRequestWireRoundTripTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.netmgt.poller.client.rpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.net.InetAddress; + +import org.junit.Test; +import org.opennms.core.xml.XmlHandler; +import org.opennms.netmgt.config.pagesequence.Page; +import org.opennms.netmgt.config.pagesequence.PageSequence; + +public class PollerRequestWireRoundTripTest { + + @Test + public void wireRoundTripWithSnmpAgentConfig() throws Exception { + final PollerRequestDTO original = PollerRequestDTOTest.getPollerRequestWithAgentConfig(); + final XmlHandler handler = XmlHandler.forWire(PollerRequestDTO.class); + + final String xml = handler.marshal(original); + assertFalse(xml.contains("\n ")); + + final PollerRequestDTO restored = handler.unmarshal(xml); + assertEquals(original, restored); + } + + @Test + public void wireRoundTripWithPageSequence() throws Exception { + final PageSequence pageSequence = new PageSequence(); + final Page page = new Page(); + page.setPath("/health"); + pageSequence.addPage(page); + + final PollerRequestDTO original = new PollerRequestDTO(); + original.setLocation("MINION"); + original.setClassName("org.opennms.netmgt.poller.monitors.HttpMonitor"); + original.setAddress(InetAddress.getByName("127.0.0.1")); + original.addAttribute("page-sequence", pageSequence); + + final XmlHandler handler = XmlHandler.forWire(PollerRequestDTO.class); + final String xml = handler.marshal(original); + assertFalse(xml.contains("\n ")); + + final PollerRequestDTO restored = handler.unmarshal(xml); + assertEquals(original, restored); + } + + @Test + public void wireRoundTripWithNestedAttribute() throws Exception { + final PollerRequestDTO original = PollerRequestDTOTest.getPollerRequestWithObject(); + final XmlHandler handler = XmlHandler.forWire(PollerRequestDTO.class); + + final PollerRequestDTO restored = handler.unmarshal(handler.marshal(original)); + assertEquals(original, restored); + } +} diff --git a/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/poller/PollerWireClassObjectAdapter.java b/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/poller/PollerWireClassObjectAdapter.java new file mode 100644 index 000000000000..926df94d933a --- /dev/null +++ b/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/poller/PollerWireClassObjectAdapter.java @@ -0,0 +1,36 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.netmgt.config.poller; + +import org.opennms.core.xml.JaxbUtils; + +/** + * {@link PollerClassObjectAdapter} variant for RPC wire payloads: compact embedded XML + * without marshal-time XSD validation. + */ +public class PollerWireClassObjectAdapter extends PollerClassObjectAdapter { + + @Override + protected JaxbUtils.MarshallerProfile marshalProfile() { + return JaxbUtils.MarshallerProfile.WIRE; + } +} diff --git a/opennms-config-jaxb/src/test/java/org/opennms/netmgt/config/poller/PollerClassObjectAdapterProfileTest.java b/opennms-config-jaxb/src/test/java/org/opennms/netmgt/config/poller/PollerClassObjectAdapterProfileTest.java new file mode 100644 index 000000000000..f0737f27a342 --- /dev/null +++ b/opennms-config-jaxb/src/test/java/org/opennms/netmgt/config/poller/PollerClassObjectAdapterProfileTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.netmgt.config.poller; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.opennms.core.xml.JaxbUtils; +import org.opennms.core.xml.JaxbUtils.MarshallerProfile; +import org.opennms.core.xml.MarshallingResourceFailureException; +import org.opennms.netmgt.config.pagesequence.Page; +import org.opennms.netmgt.config.pagesequence.PageSequence; +import org.w3c.dom.Element; + +public class PollerClassObjectAdapterProfileTest { + + @Test + public void configAdapterUsesConfigProfile() throws Exception { + final PageSequence pageSequence = samplePageSequence(); + final PollerClassObjectAdapter adapter = new PollerClassObjectAdapter(); + + final Element element = (Element) adapter.marshal(pageSequence); + assertNotNull(element); + assertEquals("page-sequence", element.getLocalName()); + + final PageSequence restored = (PageSequence) adapter.unmarshal(element); + assertEquals(pageSequence.getPages().size(), restored.getPages().size()); + assertEquals(pageSequence.getPages().get(0).getPath(), restored.getPages().get(0).getPath()); + } + + @Test + public void wireAdapterUsesWireProfile() throws Exception { + final PageSequence pageSequence = samplePageSequence(); + final PollerWireClassObjectAdapter adapter = new PollerWireClassObjectAdapter(); + + final Element element = (Element) adapter.marshal(pageSequence); + assertNotNull(element); + assertEquals("page-sequence", element.getLocalName()); + + final PageSequence restored = (PageSequence) adapter.unmarshal(element); + assertEquals(pageSequence.getPages().size(), restored.getPages().size()); + assertEquals(pageSequence.getPages().get(0).getPath(), restored.getPages().get(0).getPath()); + } + + @Test + public void invalidPageSequenceFailsConfigMarshalButSucceedsOnWire() { + final PageSequence invalid = invalidPageSequence(); + + try { + JaxbUtils.marshalToDomElement(invalid, MarshallerProfile.CONFIG); + fail("CONFIG marshal should reject XSD-invalid PageSequence"); + } catch (final MarshallingResourceFailureException e) { + // expected + } + + final Element wireElement = JaxbUtils.marshalToDomElement(invalid, MarshallerProfile.WIRE); + assertNotNull(wireElement); + assertEquals("page-sequence", wireElement.getLocalName()); + } + + @Test + public void configAdapterRejectsInvalidPageSequence() throws Exception { + final PollerClassObjectAdapter adapter = new PollerClassObjectAdapter(); + try { + adapter.marshal(invalidPageSequence()); + fail("CONFIG adapter should reject XSD-invalid PageSequence"); + } catch (final IllegalArgumentException e) { + assertTrue(e.getCause() instanceof MarshallingResourceFailureException); + } + } + + @Test + public void wireAdapterDomOutputIsMoreCompactThanConfigAdapter() throws Exception { + final PageSequence pageSequence = samplePageSequence(); + final String wireXml = elementToString( + (Element) new PollerWireClassObjectAdapter().marshal(pageSequence)); + final String configXml = elementToString( + (Element) new PollerClassObjectAdapter().marshal(pageSequence)); + + assertFalse(wireXml.contains("\n ")); + assertTrue(configXml.length() >= wireXml.length()); + } + + @Test + public void configAdapterDomMarshalMatchesJaxbUtilsConfigMarshal() throws Exception { + final PageSequence pageSequence = samplePageSequence(); + final PollerClassObjectAdapter adapter = new PollerClassObjectAdapter(); + + final Element element = (Element) adapter.marshal(pageSequence); + final PageSequence fromElement = JaxbUtils.unmarshal(PageSequence.class, elementToString(element), false); + final PageSequence fromConfig = JaxbUtils.unmarshal(PageSequence.class, + JaxbUtils.marshal(pageSequence), false); + + assertEquals(fromConfig.getPages().get(0).getPath(), fromElement.getPages().get(0).getPath()); + } + + private static PageSequence samplePageSequence() { + final PageSequence pageSequence = new PageSequence(); + final Page page = new Page(); + page.setPath("/Login.do"); + pageSequence.addPage(page); + return pageSequence; + } + + /** XSD requires at least one {@code page} child; an empty sequence is invalid. */ + private static PageSequence invalidPageSequence() { + return new PageSequence(); + } + + private static String elementToString(final Element element) throws Exception { + final javax.xml.transform.Transformer transformer = javax.xml.transform.TransformerFactory.newInstance().newTransformer(); + final java.io.StringWriter writer = new java.io.StringWriter(); + transformer.transform(new javax.xml.transform.dom.DOMSource(element), + new javax.xml.transform.stream.StreamResult(writer)); + return writer.toString(); + } +}