diff --git a/src/main/resources/io/cryostat/core/jmcagent/jfrprobes_schema.xsd b/src/main/resources/io/cryostat/core/jmcagent/jfrprobes_schema.xsd
new file mode 100644
index 00000000..445e3f8c
--- /dev/null
+++ b/src/main/resources/io/cryostat/core/jmcagent/jfrprobes_schema.xsd
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+ Global configuration options
+
+
+
+
+
+
+
+
+
+
+ This is the prefix to use when generating event class names
+
+
+
+
+
+ Will allow the recording of arrays and object parameters as Strings. This will cause toString to
+ be called for array elements and objects other than strings, which in turn can cause trouble if
+ the toString method is badly implemented. Use with care.
+
+
+
+
+
+
+ Allows converters to be used. If a converter is badly implemented, you are on your own.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ the fully qualified class name (FQCN) of the class to be transformed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ see §4.3.3 in Java Virtual Machine Specification
+
+
+
+
+
+
+
+
+
+ only if we allow toString
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ see com.oracle.jrockit.jfr.ContentType
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ a unique URI signifying a relationship between different events based on the values of specific
+ fields
+
+
+
+
+
+
+
+ the fully qualified class name (FQCN) of the converter used
+
+
+
+
+
+
+ This will only work if we allow toString
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ location {ENTRY, EXIT, WRAP}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ an expression in a subset of primary expressions (see §15.8 in Java Language Specification) to be
+ evaluated
+
+
+
+
+
+
+
+
diff --git a/src/test/java/io/cryostat/core/jmcagent/jmcagent/ProbeValidatorTest.java b/src/test/java/io/cryostat/core/jmcagent/jmcagent/ProbeValidatorTest.java
new file mode 100644
index 00000000..62233b62
--- /dev/null
+++ b/src/test/java/io/cryostat/core/jmcagent/jmcagent/ProbeValidatorTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright The Cryostat Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 io.cryostat.core.jmcagent;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+@ExtendWith(MockitoExtension.class)
+public class ProbeValidatorTest {
+
+ final String VALID_XML =
+ "\n"
+ + //
+ "\t\n"
+ + //
+ "\t\t\n"
+ + //
+ "\t\t\t\n"
+ + //
+ "\t\t\tDefined in the xml file and added by the"
+ + " agent.\n"
+ + //
+ "\t\t\tdemo/jfrhelloworldevent1\n"
+ + //
+ "\t\t\ttrue\n"
+ + //
+ "\t\t\torg.openjdk.jmc.agent.test.InstrumentMe\n"
+ + //
+ "\t\t\t\n"
+ + //
+ "\t\t\t\tprintHelloWorldJFR1\n"
+ + //
+ "\t\t\t\t()V\n"
+ + //
+ "\t\t\t\n"
+ + //
+ "\t\t\t\n"
+ + //
+ "\t\t\tWRAP\n"
+ + //
+ "\t\t\t\n"
+ + //
+ "\t\t\t\t\n"
+ + //
+ "\t\t\t\t\t'InstrumentMe.STATIC_STRING_FIELD'\n"
+ + //
+ "\t\t\t\t\tCapturing outer class field with class name prefixed"
+ + " field name\n"
+ + //
+ "\t\t\t\t\tInstrumentMe.STATIC_STRING_FIELD\n"
+ + //
+ "\t\t\t\t\n"
+ + //
+ "\t\t\t\n"
+ + //
+ "\t\t\n"
+ + //
+ "\t\n"
+ + //
+ "";
+
+ final String INVALID_XML =
+ "\n"
+ + //
+ "\t\n"
+ + //
+ "\t\n"
+ + //
+ "'This XML is not a valid probe template'\n"
+ + //
+ "\t\n"
+ + //
+ "\t\n"
+ + //
+ "\t";
+
+ ProbeValidator validator;
+
+ @BeforeEach
+ void setUp() {
+ validator = new ProbeValidator();
+ }
+
+ @Test
+ void shouldThrowProbeValidationErrorOnFailure() {
+ JMCAgentXMLStream stream =
+ new JMCAgentXMLStream(
+ new ByteArrayInputStream(INVALID_XML.getBytes(StandardCharsets.UTF_8)));
+ Assertions.assertThrows(
+ ProbeValidationException.class,
+ () -> {
+ validator.validate(new StreamSource(stream));
+ });
+ }
+
+ @Test
+ void shouldSuccessfullyValidateCorrectTemplate() {
+ JMCAgentXMLStream stream =
+ new JMCAgentXMLStream(
+ new ByteArrayInputStream(VALID_XML.getBytes(StandardCharsets.UTF_8)));
+ try {
+ validator.validate(new StreamSource(stream));
+ } catch (Exception e) {
+ System.out.println(e.toString());
+ Assertions.fail();
+ }
+ }
+
+ @Test
+ void shouldNotAllowOverridingErrorHandler() {
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> {
+ validator.setErrorHandler(new TestErrorHandler());
+ });
+ }
+
+ private static class TestErrorHandler implements ErrorHandler {
+
+ @Override
+ public void warning(SAXParseException exception) throws SAXException {
+ throw new SAXException("We shouldn't have reached here");
+ }
+
+ @Override
+ public void error(SAXParseException exception) throws SAXException {
+ throw new SAXException("We shouldn't have reached here");
+ }
+
+ @Override
+ public void fatalError(SAXParseException exception) throws SAXException {
+ throw new SAXException("We shouldn't have reached here");
+ }
+ }
+}