diff --git a/.classpath b/.classpath index f9034dd..4da6324 100644 --- a/.classpath +++ b/.classpath @@ -1,38 +1,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/axonivy/jmx/internal/Instruction.java b/src/main/java/com/axonivy/jmx/internal/Instruction.java index 845bd6a..e040f16 100644 --- a/src/main/java/com/axonivy/jmx/internal/Instruction.java +++ b/src/main/java/com/axonivy/jmx/internal/Instruction.java @@ -38,8 +38,8 @@ * } * } * ----- - * Instruction.parseInstruction(manager, MyBean.class "Hello #{name}").execute(new MyBean()) -> "Hello Reto" - * Instruction.parseInstruction(manager, MyBean.class "Hello #{application.description}").execute(new MyBean()) -> "Hello Weiss" + * Instruction.parseInstruction(manager, MyBean.class, "Hello #{name}").execute(new MyBean()) -> "Hello Reto" + * Instruction.parseInstruction(manager, MyBean.class, "Hello #{application.description}").execute(new MyBean()) -> "Hello Weiss" * * * diff --git a/src/main/java/com/axonivy/jmx/internal/MBeanType.java b/src/main/java/com/axonivy/jmx/internal/MBeanType.java index 73f1208..011e0ad 100644 --- a/src/main/java/com/axonivy/jmx/internal/MBeanType.java +++ b/src/main/java/com/axonivy/jmx/internal/MBeanType.java @@ -19,7 +19,7 @@ class MBeanType private MBeanManager manager; private Class mBeanClass; private Instruction descriptionInstruction; - private Instruction nameInstruction; + private NameInstruction nameInstruction; private List compositionReferenceInfos; MBeanType(MBeanManager manager, Class mBeanClass) @@ -53,7 +53,7 @@ String evaluateName(Object mBean) { if (nameInstruction == null) { - nameInstruction = Instruction.parseInstruction(manager, mBeanClass, annotation.value()); + nameInstruction = NameInstruction.parseInstruction(manager, mBeanClass, annotation.value()); } return nameInstruction.execute(mBean); } diff --git a/src/main/java/com/axonivy/jmx/internal/NameInstruction.java b/src/main/java/com/axonivy/jmx/internal/NameInstruction.java new file mode 100644 index 0000000..bdfdf13 --- /dev/null +++ b/src/main/java/com/axonivy/jmx/internal/NameInstruction.java @@ -0,0 +1,163 @@ +package com.axonivy.jmx.internal; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.commons.lang3.StringUtils; + +class NameInstruction +{ + private ObjectName template; + private boolean isRelative; + private List> valueInstructions; + + private NameInstruction(MBeanManager manager, Class mBeanClass, String instruction) throws MalformedObjectNameException + { + isRelative = isRelative(instruction); + if (isRelative) + { + instruction = ":"+instruction; + } + template = new ObjectName(instruction); + List> properties = getProperties(); + valueInstructions = properties + .stream() + .map(property -> new Property<>(property.getKey(), Instruction.parseInstruction(manager, mBeanClass, property.getValue()))) + .collect(Collectors.toList()); + } + + static NameInstruction parseInstruction(MBeanManager manager, Class mBeanClass, String value) + { + try + { + return new NameInstruction(manager, mBeanClass, value); + } + catch(MalformedObjectNameException ex) + { + throw new IllegalArgumentException("Object name of MBean '"+value+"'is malformed", ex); + } + } + + String execute(Object mBean) + { + List> evaluatedValues = evaluateAndQuoteValues(mBean); + String name = buildName(evaluatedValues); + return name; + } + + private boolean isRelative(String instruction) + { + int firstColon = StringUtils.indexOf(instruction, ":"); + if (firstColon < 0) + { + return true; + } + int firstEqual = StringUtils.indexOf(instruction, "="); + return firstEqual> getProperties() + { + List> properties = template + .getKeyPropertyList() + .entrySet() + .stream() + .map(entry -> new Property<>(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + String strName = template.getKeyPropertyListString(); + Collections.sort(properties, new PropertyComparator(strName)); + return properties; + } + + private List> evaluateAndQuoteValues(Object mBean) + { + List> evaluatedValues = valueInstructions + .stream() + .map(property -> new Property<>(property.getKey(), property.getValue().execute(mBean))) + .map(this::quoteIfNecessary) + .collect(Collectors.toList()); + return evaluatedValues; + } + + private String buildName(List> evaluatedValues) + { + StringBuilder name = new StringBuilder(256); + if (!isRelative) + { + name.append(template.getDomain()); + name.append(':'); + } + String properties = evaluatedValues + .stream() + .map(Property::toString) + .collect(Collectors.joining(",")); + name.append(properties); + return name.toString(); + } + + private Property quoteIfNecessary(Property property) + { + if (needsQuoting(property.getValue())) + { + return new Property(property.getKey(), ObjectName.quote(property.getValue())); + } + return property; + } + + private boolean needsQuoting(String value) + { + boolean isQuoted = StringUtils.startsWith(value, "\"") && StringUtils.endsWith(value, "\""); + return !isQuoted && StringUtils.containsAny(value, ',',':','=','\n', '\"'); + } + + private static final class Property + { + private String key; + private T value; + + private Property(String key, T value) + { + this.key = key; + this.value = value; + } + + private T getValue() + { + return value; + } + + private String getKey() + { + return key; + } + + @Override + public String toString() + { + return key+"="+value; + } + } + + private static final class PropertyComparator implements Comparator> + { + private String strName; + + private PropertyComparator(String strName) + { + this.strName = strName; + } + + @Override + public int compare(Property property1, Property property2) + { + int positionProperty1 = StringUtils.indexOf(strName, property1.getKey()+"="); + int positionProperty2 = StringUtils.indexOf(strName, property2.getKey()+"="); + return Integer.compare(positionProperty1, positionProperty2); + } + } +} diff --git a/src/test/java/com/axonivy/jmx/TestMBeanNameQuoting.java b/src/test/java/com/axonivy/jmx/TestMBeanNameQuoting.java new file mode 100644 index 0000000..0e26491 --- /dev/null +++ b/src/test/java/com/axonivy/jmx/TestMBeanNameQuoting.java @@ -0,0 +1,100 @@ +package com.axonivy.jmx; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.Test; + +public class TestMBeanNameQuoting +{ + @Test + public void quotedBean() throws MalformedObjectNameException + { + assertThat(isRegistered("Test:type=\",=:\\n \\\" hi\"")).isFalse(); + MBeans.registerMBeanFor(new QuotedMBean()); + assertThat(isRegistered("Test:type=\",=:\\n \\\" hi\"")).isTrue(); + } + + @Test + public void notQuotedBeanFromElExpression() throws MalformedObjectNameException + { + assertNameNotQuoted("hello world"); + } + + @Test + public void quotedBeanFromElExpressionWithComma() throws MalformedObjectNameException + { + assertNameQuoted("hello,world"); + } + + @Test + public void quotedBeanFromElExpressionWithEqual() throws MalformedObjectNameException + { + assertNameQuoted("hello=world"); + } + + @Test + public void quotedBeanFromElExpressionWithCollon() throws MalformedObjectNameException + { + assertNameQuoted("hello:world"); + } + + @Test + public void quotedBeanFromElExpressionWithNewLine() throws MalformedObjectNameException + { + assertNameQuoted("hello\nworld"); + } + + @Test + public void quotedBeanFromElExpressionWithQuote() throws MalformedObjectNameException + { + assertNameQuoted("hello\"world"); + } + + @Test + public void notQuotedBeanFromElExpressionThatIsAlreadyQuoted() throws MalformedObjectNameException + { + assertNameNotQuoted("\"hello,=:\\n\\\"world\""); + } + + private static void assertNameNotQuoted(String name) throws MalformedObjectNameException + { + assertName(name, name); + } + + private void assertNameQuoted(String name) throws MalformedObjectNameException + { + assertName(name, ObjectName.quote(name)); + } + + private static void assertName(String name, String mBeanName) throws MalformedObjectNameException + { + assertThat(isRegistered("Test:name="+mBeanName)).isFalse(); + MBeans.registerMBeanFor(new NameMBean(name)); + assertThat(isRegistered("Test:name="+mBeanName)).isTrue(); + } + + @MBean(value="Test:type=\",=:\\n \\\" hi\"") + private static final class QuotedMBean + { + } + + @MBean(value="Test:name=#{name}") + private static final class NameMBean + { + @SuppressWarnings("unused") + private String name; + + private NameMBean(String name) + { + this.name = name; + } + } + + private static boolean isRegistered(String name) throws MalformedObjectNameException + { + return MBeans.getMBeanServer().isRegistered(new ObjectName(name)); + } +}