8264124: Update MXBean specification and implementation to extend mapping of CompositeType to records

Reviewed-by: mchung, chegar, alanb
This commit is contained in:
Daniel Fuchs 2021-04-12 16:31:36 +00:00
parent 714ae54f91
commit d84a7e55be
3 changed files with 860 additions and 34 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2015, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -443,9 +443,11 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
if (gcInfoHack && propertyName.equals("CompositeType"))
continue;
Method old =
getterMap.put(decapitalize(propertyName),
method);
// Don't decapitalize if this is a record component name.
// We only decapitalize for getXxxx(), isXxxx(), and setXxxx()
String name = c.isRecord() && method.getName().equals(propertyName)
? propertyName : decapitalize(propertyName);
Method old = getterMap.put(name, method);
if (old != null) {
final String msg =
"Class " + c.getName() + " has method name clash: " +
@ -862,6 +864,9 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
{
new CompositeBuilderViaFrom(targetClass, itemNames),
},
{
new RecordCompositeBuilder(targetClass, itemNames),
},
{
new CompositeBuilderViaConstructor(targetClass, itemNames),
},
@ -1139,14 +1144,14 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
/** Builder for when the target class has a constructor that is
annotated with {@linkplain ConstructorParameters @ConstructorParameters}
or {@code @ConstructorProperties} so we can see the correspondence to getters. */
private static final class CompositeBuilderViaConstructor
private static class CompositeBuilderViaConstructor
extends CompositeBuilder {
CompositeBuilderViaConstructor(Class<?> targetClass, String[] itemNames) {
super(targetClass, itemNames);
}
private String[] getConstPropValues(Constructor<?> ctr) {
String[] getConstPropValues(Constructor<?> ctr) {
// is constructor annotated by javax.management.ConstructorParameters ?
ConstructorParameters ctrProps = ctr.getAnnotation(ConstructorParameters.class);
if (ctrProps != null) {
@ -1171,8 +1176,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
}
if (annotatedConstrList.isEmpty())
return "no constructor has either @ConstructorParameters " +
"or @ConstructorProperties annotation";
return reportNoConstructor();
annotatedConstructors = newList();
@ -1196,9 +1200,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
// so we can test unambiguity.
Set<BitSet> getterIndexSets = newSet();
for (Constructor<?> constr : annotatedConstrList) {
String annotationName =
constr.isAnnotationPresent(ConstructorParameters.class) ?
"@ConstructorParameters" : "@ConstructorProperties";
String matchingMechanism = matchingMechanism(constr);
String[] propertyNames = getConstPropValues(constr);
@ -1206,7 +1208,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
if (paramTypes.length != propertyNames.length) {
final String msg =
"Number of constructor params does not match " +
annotationName + " annotation: " + constr;
referenceMechannism(matchingMechanism) +": " + constr;
throw new InvalidObjectException(msg);
}
@ -1219,7 +1221,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
String propertyName = propertyNames[i];
if (!getterMap.containsKey(propertyName)) {
String msg =
annotationName + " includes name " + propertyName +
matchingMechanism + " includes name " + propertyName +
" which does not correspond to a property";
for (String getterName : getterMap.keySet()) {
if (getterName.equalsIgnoreCase(propertyName)) {
@ -1234,7 +1236,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
paramIndexes[getterIndex] = i;
if (present.get(getterIndex)) {
final String msg =
annotationName + " contains property " +
matchingMechanism + " contains property " +
propertyName + " more than once: " + constr;
throw new InvalidObjectException(msg);
}
@ -1243,7 +1245,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
Type propertyType = getter.getGenericReturnType();
if (!propertyType.equals(paramTypes[i])) {
final String msg =
annotationName + " gives property " + propertyName +
matchingMechanism + " gives property " + propertyName +
" of type " + propertyType + " for parameter " +
" of type " + paramTypes[i] + ": " + constr;
throw new InvalidObjectException(msg);
@ -1252,10 +1254,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
if (!getterIndexSets.add(present)) {
final String msg =
"More than one constructor has " +
"@ConstructorParameters or @ConstructorProperties " +
"annotation with this set of names: " +
Arrays.toString(propertyNames);
reportMultipleConstructorsFoundFor(propertyNames);
throw new InvalidObjectException(msg);
}
@ -1292,10 +1291,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
i = u.nextSetBit(i+1))
names.add(itemNames[i]);
final String msg =
"Constructors with @ConstructorParameters or " +
"@ConstructorProperties annotation " +
"would be ambiguous for these items: " +
names;
reportConstructorsAmbiguousFor(names);
throw new InvalidObjectException(msg);
}
}
@ -1305,7 +1301,41 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
return null; // success!
}
final Object fromCompositeData(CompositeData cd,
String reportNoConstructor() {
return "no constructor has either @ConstructorParameters " +
"or @ConstructorProperties annotation";
}
String matchingMechanism(Constructor<?> constr) {
return constr.isAnnotationPresent(ConstructorParameters.class) ?
"@ConstructorParameters" : "@ConstructorProperties";
}
String referenceMechannism(String matchingMechanism) {
return matchingMechanism + " annotation";
}
String reportMultipleConstructorsFoundFor(String... propertyNames) {
return "More than one constructor has " +
"@ConstructorParameters or @ConstructorProperties " +
"annotation with this set of names: " +
Arrays.toString(propertyNames);
}
String reportConstructorsAmbiguousFor(Set<String> names) {
return "Constructors with @ConstructorParameters or " +
"@ConstructorProperties annotation " +
"would be ambiguous for these items: " +
names;
}
String reportNoConstructorFoundFor(Set<String> names) {
return "No constructor has either @ConstructorParameters " +
"or @ConstructorProperties annotation for this set of " +
"items: " + names;
}
Object fromCompositeData(CompositeData cd,
String[] itemNames,
MXBeanMapping[] mappings)
throws InvalidObjectException {
@ -1330,10 +1360,7 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
}
if (max == null) {
final String msg =
"No constructor has either @ConstructorParameters " +
"or @ConstructorProperties annotation for this set of " +
"items: " + ct.keySet();
final String msg = reportNoConstructorFoundFor(ct.keySet());
throw new InvalidObjectException(msg);
}
@ -1379,6 +1406,73 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
private List<Constr> annotatedConstructors;
}
/** Builder for when the target class is a record */
private static final class RecordCompositeBuilder
extends CompositeBuilderViaConstructor {
RecordCompositeBuilder(Class<?> targetClass, String[] itemNames) {
super(targetClass, itemNames);
}
String[] getConstPropValues(Constructor<?> ctor) {
var components = getTargetClass().getRecordComponents();
var ptypes = ctor.getGenericParameterTypes();
if (components.length != ptypes.length) {
return super.getConstPropValues(ctor);
}
var len = components.length;
String[] res = new String[len];
for (int i=0; i < len ; i++) {
if (!ptypes[i].equals(components[i].getGenericType())) {
return super.getConstPropValues(ctor);
}
res[i] = components[i].getName();
}
return res;
}
String applicable(Method[] getters) throws InvalidObjectException {
Class<?> targetClass = getTargetClass();
if (!targetClass.isRecord())
return "class is not a record";
return super.applicable(getters);
}
@Override
Object fromCompositeData(CompositeData cd, String[] itemNames, MXBeanMapping[] mappings)
throws InvalidObjectException {
return super.fromCompositeData(cd, itemNames, mappings);
}
String reportNoConstructor() {
return "canonical constructor for record not found";
}
String matchingMechanism(Constructor<?> constr) {
return "canonical constructor";
}
String referenceMechannism(String matchingMechanism) {
return matchingMechanism;
}
String reportMultipleConstructorsFoundFor(String... propertyNames) {
return "More than one constructor has this set of names: " +
Arrays.toString(propertyNames);
}
String reportConstructorsAmbiguousFor(Set<String> names) {
return "Constructors would be ambiguous for these items: " +
names;
}
String reportNoConstructorFoundFor(Set<String> names) {
return "No constructor has this set of " +
"items: " + names;
}
}
/** Builder for when the target class is an interface and contains
no methods other than getters. Then we can make an instance
using a dynamic proxy that forwards the getters to the source
@ -1504,7 +1598,16 @@ public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {
public static String propertyName(Method m) {
String rest = null;
String name = m.getName();
if (name.startsWith("get"))
var c = m.getDeclaringClass();
if (c.isRecord()) {
for (var rc : c.getRecordComponents()) {
if (name.equals(rc.getName())
&& m.getReturnType() == rc.getType()) {
rest = name;
break;
}
}
} else if (name.startsWith("get"))
rest = name.substring(3);
else if (name.startsWith("is") && m.getReturnType() == boolean.class)
rest = name.substring(2);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -30,6 +30,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.RecordComponent;
// remaining imports are for Javadoc
import java.io.InvalidObjectException;
@ -531,6 +532,13 @@ public class MemoryPool
<td>{@link TabularData}<br>
(see below)</td>
</tr>
<tr>
<th scope="row">{@linkplain Record Record classes}</th>
<td>{@link CompositeType}, if possible<br>
(see below)</td>
<td>{@link CompositeData}<br>
(see below)</td>
</tr>
<tr>
<th scope="row">An MXBean interface</th>
<td>{@code SimpleType.OBJECTNAME}<br>
@ -543,7 +551,8 @@ public class MemoryPool
<td>{@link CompositeType},
if possible<br>
(see below)</td>
<td>{@link CompositeData}</td>
<td>{@link CompositeData}<br>
(see below)</td>
</tbody>
</table>
@ -656,6 +665,60 @@ TabularType tabularType =
TabularData} that serializes as {@code TabularDataSupport}.</p>
<h3 id="records">Mappings for Records</h3>
<p>A {@linkplain Record record} class <em>J</em> can be converted
to a {@link CompositeType} if and only if all its
{@linkplain Class#getRecordComponents() components} are
convertible to open types. Otherwise, it is not convertible.
A record that has no components is not convertible.</p>
<h4 id="record-type-map">Mapping a record class to
{@code CompositeType}</h4>
<p>A record whose components are all convertible to open
types, is itself convertible to a {@link CompositeType}.
The record class is converted to a {@code CompositeType}
as follows.</p>
<ul>
<li>The type name of the {@code CompositeType} is the name
of the record class.</li>
<li>The record getters are the accessors for the
{@linkplain RecordComponent record components}.</li>
<li>For each record component of type <em>T</em>, the item in
the {@code CompositeType} has the same name as the record
component and its type is <em>opentype(T)</em>, as
defined by the <a href="#mapping-rules">type mapping rules</a>
above.</li>
</ul>
<h4 id="record-data-map">Mapping an instance of a record class to
{@code CompositeData}</h4>
<p>The mapping from an instance of a record class to a
{@link CompositeData} corresponding to the {@code CompositeType}
is the same as specified for
<a href="#composite-data-map">other types</a>.</p>
<h4 id="reconstructing-record">Reconstructing an instance of a record class
from a {@code CompositeData}</h4>
<p>A record is reconstructed using its canonical constructor.
The canonical constructor doesn't require the presence of
{@link ConstructorParameters &#64;javax.management.ConstructorParameters}
or {@code @java.beans.ConstructorProperties} annotations. If these
annotations are present on the canonical constructor they
will be ignored.</p>
<p>How an instance of a record class <em>J</em> is reconstructed
from a {@link CompositeData} is detailed in
<a href="#reconstructing">Reconstructing an instance
of Java type or record class <em>J</em> from a {@code CompositeData}</a>
below.</p>
<h3 id="mxbean-map">Mappings for MXBean interfaces</h3>
<p>An MXBean interface, or a type referenced within an MXBean
@ -753,6 +816,9 @@ public interface ModuleMXBean {
{@code CompositeType} is determined by the <a href="#type-names">
type name rules</a> below.</p>
<h4 id="composite-type-map">Mapping a Java type <em>J</em>
to {@link CompositeType}</h4>
<p>The class is examined for getters using the conventions
<a href="#naming-conv">above</a>. (Getters must be public
instance methods.) If there are no getters, or if
@ -796,6 +862,9 @@ public interface ModuleMXBean {
getOwner} and {@code isOwner}, or {@code getOwner} and {@code
getowner}) then the type is not convertible.</p>
<h4 id="composite-data-map" >Mapping from an instance of Java
type or record class <em>J</em> to {@code CompositeData}</h4>
<p>When the Open Type is {@code CompositeType}, the corresponding
mapped Java type (<em>opendata(J)</em>) is {@link
CompositeData}. The mapping from an instance of <em>J</em> to a
@ -809,7 +878,7 @@ public interface ModuleMXBean {
Open Data type. Thus, a getter such as</p>
<blockquote>
{@code List<String> getNames()}
{@code List<String> getNames()} (or {@code List<String> names()} for a record)
</blockquote>
<p>will have been mapped to an item with name "{@code names}" and
@ -825,8 +894,8 @@ public interface ModuleMXBean {
CompositeDataSupport}.</p>
<h4>Reconstructing an instance of Java type <em>J</em> from
a {@code CompositeData}</h4>
<h4 id="reconstructing">Reconstructing an instance of Java type
or record class <em>J</em> from a {@code CompositeData}</h4>
<p>If <em>opendata(J)</em> is {@code CompositeData} for a Java type
<em>J</em>, then either an instance of <em>J</em> can be
@ -846,6 +915,17 @@ public interface ModuleMXBean {
then that method is called to reconstruct an instance of
<em>J</em>.</p></li>
<li><p>Otherwise, if <em>J</em> is a {@link Record} class,
and the record canonical constructor is applicable,
an instance of <em>J</em> is reconstructed by calling
the record canonical constructor.
The canonical constructor, if applicable, is called
with the appropriate reconstructed items from the
{@code CompositeData}. The canonical constructor
is <em>applicable</em> if all the properties named
by the record components are present in the
{@code CompositeData}.</p></li>
<li><p>Otherwise, if <em>J</em> has at least one public
constructor with either {@link javax.management.ConstructorParameters
&#64;javax.management.ConstructorParameters} or
@ -962,6 +1042,15 @@ public class NamedNumber {
</blockquote>
</li>
<li>Record:
<blockquote>
<pre>
public record NamedNumber(int number, String name) {}
</pre>
</blockquote>
</li>
<li>Public constructor with <code>&#64;ConstructorParameters</code> annotation:
<blockquote>

View File

@ -0,0 +1,634 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.InvalidObjectException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.management.Attribute;
import javax.management.ConstructorParameters;
import javax.management.JMX;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerFactory;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.management.StandardMBean;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeDataView;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
/**
* @test
* @bug 8264124
* @run testng RecordsMXBeanTest
*/
public class RecordsMXBeanTest {
// Simple record with open types
public record Data(List<Integer> ints, Map<String, List<String>> map) {}
// Used to test case in component names
public record MixedCases(int Foo, int BarBar, int foo) {}
// Used to test nested records
public record DataPoint(Data x, Data y, MixedCases mixed) {}
// Used to test reconstruction using a non-canonical constructor
public record Annotated(int x, int y, int z) {
@ConstructorParameters(value = {"y", "x"})
public Annotated(int y, int x) {
this(x,y,-1);
}
}
// Used to test reconstruction using a static `from` method
public record FromMethod(int x, int y, int z) {
public static FromMethod from(CompositeData cd) {
int x = (int) cd.get("x");
int y = (int) cd.get("y");
int z = -x -y;
return new FromMethod(x, y, z);
}
}
// A record that exposes methods that look like
// getters... These should be ignored - only the
// record components should be considered.
public record Trickster(int x, int y) {
public int getZ() { return -x() -y(); }
public boolean isTricky() { return true; }
}
// A regular class similar to the Trickster,
// but this time z and tricky should appear
// in the composite data
public static class TricksterToo {
final int x;
final int y;
@ConstructorParameters({"x", "y"})
public TricksterToo(int x, int y) {
this.x = x; this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
public int getZ() { return -x -y; }
public boolean isTricky() { return true; }
}
// A record with a conflicting name getX/x which
// should ensure that non component getters are ignored
public record RWithGetter(int x, int y) {
public int getX() { return x;}
}
// A record with an annotated cannonical constructor.
// Annotation should be ignored
public record WithAnno(int x, int y) {
@ConstructorParameters({"y", "x"})
public WithAnno(int x, int y) {
this.x = x;
this.y = y;
}
}
// A record that implements CompositeDataView
public record WithCDV(int x, int y) implements CompositeDataView {
@Override
public CompositeData toCompositeData(CompositeType ct) {
if (ct == null) return null;
try {
return new CompositeDataSupport(ct, new String[]{"x", "y"}, new Object[]{x() + 1, y() + 2});
} catch (OpenDataException x) {
throw new IllegalArgumentException(ct.getTypeName(), x);
}
}
}
// A read only MXBean interface
public interface RecordsMXBean {
public Data getData();
public DataPoint getDataPoint();
public default Map<String, DataPoint> allPoints() {
return Map.of("allpoints", getDataPoint());
}
}
// A read-write MXBean interface
public interface Records2MXBean extends RecordsMXBean {
public void setDataPoint(DataPoint point);
}
// An implementation of the read-only MXBean interface which is
// itself a record (this is already supported)
public record Records(DataPoint point) implements RecordsMXBean {
@Override
public Data getData() {
return point().x();
}
@Override
public DataPoint getDataPoint() {
return point();
}
@Override
public Map<String, DataPoint> allPoints() {
return Map.of("point", point());
}
}
// An implementation of the read-write MXBean interface
public static class Records2 implements Records2MXBean {
private volatile DataPoint point = new DataPoint(
new Data(List.of(1, 2), Map.of("foo", List.of("bar"))),
new Data(List.of(3, 4), Map.of("bar", List.of("foo"))),
new MixedCases(5, 6, 7)
);
@Override
public Data getData() {
return point.x;
}
@Override
public DataPoint getDataPoint() {
return point;
}
@Override
public void setDataPoint(DataPoint point) {
this.point = point;
}
@Override
public Map<String, DataPoint> allPoints() {
return Map.of("point", point);
}
}
// A complex MXBean interface used to test reconstruction
// of records through non-canonical annotated constructors
// and static `from` method
public interface ComplexMXBean {
Annotated getAnnotated();
void setAnnotated(Annotated annotated);
FromMethod getFromMethod();
void setFromMethod(FromMethod fromMethod);
Trickster getTrickster();
void setTrickster(Trickster trick);
TricksterToo getTricksterToo();
void setTricksterToo(TricksterToo trick);
RWithGetter getR();
void setR(RWithGetter r);
WithAnno getWithAnno();
void setWithAnno(WithAnno r);
WithCDV getCDV();
void setCDV(WithCDV cdv);
}
// An implementation of the complex MXBean interface
public static class Complex implements ComplexMXBean {
private volatile Annotated annotated = new Annotated(1, 2, 3);
private volatile FromMethod fromMethod = new FromMethod(1, 2, 3);
private volatile Trickster trickster = new Trickster(4, 5);
private volatile TricksterToo too = new TricksterToo(6, 7);
private volatile RWithGetter r = new RWithGetter(8, 9);
private volatile WithAnno withAnno = new WithAnno(10, 11);
private volatile WithCDV withCDV = new WithCDV(12, 13);
@Override
public Annotated getAnnotated() {
return annotated;
}
@Override
public void setAnnotated(Annotated annotated) {
this.annotated = annotated;
}
@Override
public FromMethod getFromMethod() {
return fromMethod;
}
@Override
public void setFromMethod(FromMethod fromMethod) {
this.fromMethod = fromMethod;
}
@Override
public Trickster getTrickster() {
return trickster;
}
@Override
public void setTrickster(Trickster trickster) {
this.trickster = trickster;
}
@Override
public TricksterToo getTricksterToo() {
return too;
}
@Override
public void setTricksterToo(TricksterToo trick) {
too = trick;
}
@Override
public RWithGetter getR() {
return r;
}
@Override
public void setR(RWithGetter r) {
this.r = r;
}
@Override
public WithAnno getWithAnno() {
return withAnno;
}
@Override
public void setWithAnno(WithAnno r) {
this.withAnno = r;
}
@Override
public WithCDV getCDV() {
return withCDV;
}
@Override
public void setCDV(WithCDV cdv) {
withCDV = cdv;
}
}
public record NonCompliantR1(int x, Object y) {
public int getX() { return x;}
}
public interface NC1MXBean {
public NonCompliantR1 getNCR1();
}
public class NC1 implements NC1MXBean {
private volatile NonCompliantR1 ncr1 = new NonCompliantR1(1,2);
@Override
public NonCompliantR1 getNCR1() {
return ncr1;
}
}
public record NonCompliantR2(int x, List<? super Integer> y) {
}
public interface NC2MXBean {
public NonCompliantR2 getNCR2();
}
public class NC2 implements NC2MXBean {
private volatile NonCompliantR2 ncr2 = new NonCompliantR2(1,List.of(2));
@Override
public NonCompliantR2 getNCR2() {
return ncr2;
}
}
public record NonCompliantR3() {
}
public interface NC3MXBean {
public NonCompliantR3 getNCR3();
}
public class NC3 implements NC3MXBean {
private volatile NonCompliantR3 ncr3 = new NonCompliantR3();
@Override
public NonCompliantR3 getNCR3() {
return ncr3;
}
}
@DataProvider(name = "wrapInStandardMBean")
Object[][] wrapInStandardMBean() {
return new Object[][] {
new Object[] {"wrapped in StandardMBean", true},
new Object[] {"not wrapped in StandardMBean", false}
};
}
@Test(dataProvider = "wrapInStandardMBean")
public void testLocal(String desc, boolean standard) throws Exception {
// test local
System.out.println("\nTest local " + desc);
MBeanServer mbs = MBeanServerFactory.newMBeanServer("test");
test(mbs, mbs, standard);
}
@Test(dataProvider = "wrapInStandardMBean")
public void testRemote(String desc, boolean standard) throws Exception {
// test remote
System.out.println("\nTest remote " + desc);
MBeanServer mbs = MBeanServerFactory.newMBeanServer("test");
final JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://");
JMXConnectorServer server =
JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs);
server.start();
try {
JMXConnector ctor = server.toJMXConnector(null);
ctor.connect();
try {
test(mbs, ctor.getMBeanServerConnection(), standard);
} finally {
ctor.close();
}
} finally {
server.stop();
}
}
private void test(MBeanServer server, MBeanServerConnection connection, boolean standard)
throws Exception {
// test RecordsMXBean via MBeanServerConnection
assertTrue(JMX.isMXBeanInterface(RecordsMXBean.class));
Records records = new Records(new DataPoint(
new Data(List.of(1, 2), Map.of("foo", List.of("bar"))),
new Data(List.of(3, 4), Map.of("bar", List.of("foo"))),
new MixedCases(5, 6, 7)
));
ObjectName recname = new ObjectName("test:type=Records");
var mbean = standard
? new StandardMBean(records, RecordsMXBean.class, true)
: records;
server.registerMBean(mbean, recname);
RecordsMXBean mxBean = JMX.newMXBeanProxy(connection, recname, RecordsMXBean.class);
Records retrieved = new Records(mxBean.getDataPoint());
assertEquals(retrieved, records);
assertEquals(mxBean.allPoints(), records.allPoints());
// test Records2MXBean via MBeanServerConnection
assertTrue(JMX.isMXBeanInterface(Records2MXBean.class));
Records2 records2 = new Records2();
assertEquals(records2.allPoints(), records.allPoints());
ObjectName recname2 = new ObjectName("test:type=Records2");
var mbean2 = standard
? new StandardMBean(records2, Records2MXBean.class, true)
: records2;
server.registerMBean(mbean2, recname2);
Records2MXBean mxBean2 = JMX.newMXBeanProxy(connection, recname2, Records2MXBean.class);
Records retrieved2 = new Records(mxBean2.getDataPoint());
assertEquals(retrieved2, records);
assertEquals(mxBean2.allPoints(), records.allPoints());
// mutate Records2MXBean via MBeanServerConnection
DataPoint point2 = new DataPoint(records.point().y(), records.point().x(), records.point().mixed());
mxBean2.setDataPoint(point2);
assertEquals(mxBean2.getDataPoint(), point2);
assertEquals(mxBean2.allPoints(), Map.of("point", point2));
// test reconstruction through non-canonical constructor and from method
Complex complex = new Complex();
var complexMBean = new StandardMBean(complex, ComplexMXBean.class, true);
ObjectName recname3 = new ObjectName("test:type=Complex");
var mbean3 = standard ? complexMBean : complex;
server.registerMBean(complexMBean, recname3);
ComplexMXBean mBean5 = JMX.newMXBeanProxy(connection, recname3, ComplexMXBean.class);
var annotated = mBean5.getAnnotated();
assertEquals(annotated, complex.getAnnotated());
// Obtain the CompositeData that corresponds to the Annotated record
var cd = (CompositeData) complexMBean.getAttribute("Annotated");
var ct = cd.getCompositeType();
// Construct a version of the "Annotated" composite data where z is missing
var nct = new CompositeType(ct.getTypeName(), ct.getDescription(), new String[] {"x", "y"},
new String[] {ct.getDescription("x"), ct.getDescription("y")},
new OpenType<?>[] {ct.getType("x"), ct.getType("y")});
var ncd = new CompositeDataSupport(nct, new String[] {"x", "y"},
new Object[] {cd.get("x"), cd.get("y")});
// send the modified composite data to remote, and check
// that the non-canonical constructor was called (this constructor
// sets z = -1)
connection.setAttribute(recname3, new Attribute("Annotated", ncd));
var annotated2 = mBean5.getAnnotated();
assertEquals(annotated2.x(), annotated.x());
assertEquals(annotated2.y(), annotated2.y());
assertEquals(annotated2.z(), -1);
// gets the FromMethod record, and check that the `from` method
// we defined was called. When reconstructed from our `from` method,
// z will be set to z = -x -y;
var from = mBean5.getFromMethod();
assertEquals(from.x(), 1);
assertEquals(from.y(), 2);
assertEquals(from.z(), -3);
mBean5.setFromMethod(new FromMethod(2, 1, 3));
from = mBean5.getFromMethod();
assertEquals(from.x(), 2);
assertEquals(from.y(), 1);
assertEquals(from.z(), -3);
// checks that the presence of getter-like methods doesn't
// prevent the record from being reconstructed.
var cdtrick = (CompositeData) connection.getAttribute(recname3, "Trickster");
println("tricky", cdtrick);
assertEquals(cdtrick.getCompositeType().keySet(), Set.of("x", "y"));
var trick = mBean5.getTrickster();
assertEquals(trick.x(), 4);
assertEquals(trick.y(), 5);
assertEquals(trick.getZ(), -9);
assertTrue(trick.isTricky());
mBean5.setTrickster(new Trickster(5, 4));
trick = mBean5.getTrickster();
assertEquals(trick.x(), 5);
assertEquals(trick.y(), 4);
assertEquals(trick.getZ(), -9);
assertTrue(trick.isTricky());
// get the "TricksterToo" composite data
var cdtoo = (CompositeData) connection.getAttribute(recname3, "TricksterToo");
println("tricky too", cdtoo);
assertEquals(cdtoo.getCompositeType().keySet(), Set.of("x", "y", "tricky", "z"));
var too = mBean5.getTricksterToo();
assertEquals(too.getX(), 6);
assertEquals(too.getY(), 7);
assertEquals(too.getZ(), -13);
assertTrue(too.isTricky());
mBean5.setTricksterToo(new TricksterToo(7, 6));
too = mBean5.getTricksterToo();
assertEquals(too.getX(), 7);
assertEquals(too.getY(), 6);
assertEquals(too.getZ(), -13);
assertTrue(too.isTricky());
// builds a composite data that contains more fields than
// the record...
var cdtype = cdtrick.getCompositeType();
var itemNames = List.of("x", "y", "z", "tricky").toArray(new String[0]);
var itemDesc = Stream.of(itemNames)
.map(cdtoo.getCompositeType()::getDescription)
.toArray(String[]::new);
var itemTypes = Stream.of(itemNames)
.map(cdtoo.getCompositeType()::getType)
.toArray(OpenType<?>[]::new);
var cdtype2 = new CompositeType(cdtype.getTypeName(),
cdtype.getDescription(), itemNames, itemDesc, itemTypes);
var values = Stream.of(itemNames).map(cdtoo::get).toArray();
var cdtrick2 = new CompositeDataSupport(cdtype2, itemNames, values);
// sets the composite data with more fields - the superfluous fields
// should be ignored...
connection.setAttribute(recname3, new Attribute("Trickster", cdtrick2));
// get the composite data we just set
var cdtrick3 = (CompositeData) connection.getAttribute(recname3, "Trickster");
assertEquals(cdtrick3.getCompositeType().keySet(), Set.of("x", "y"));
// get the "Trickster" through the MXBean proxy
var trick3 = mBean5.getTrickster();
assertEquals(trick3.x(), 6);
assertEquals(trick3.y(), 7);
assertEquals(trick3.getZ(), -13);
assertEquals(trick3.isTricky(), true);
// get record that has both x() and getX()
var rWithGetter = mBean5.getR();
assertEquals(rWithGetter.x(), rWithGetter.getX());
assertEquals(rWithGetter.x(), 8);
assertEquals(rWithGetter.y(), 9);
mBean5.setR(new RWithGetter(rWithGetter.y(), rWithGetter.x()));
rWithGetter = mBean5.getR();
assertEquals(rWithGetter.x(), rWithGetter.getX());
assertEquals(rWithGetter.x(), 9);
assertEquals(rWithGetter.y(), 8);
var withAnno = mBean5.getWithAnno();
assertEquals(withAnno.x(), 10);
assertEquals(withAnno.y(), 11);
withAnno = new WithAnno(12, 13);
mBean5.setWithAnno(withAnno);
withAnno = mBean5.getWithAnno();
assertEquals(withAnno.x(), 12);
assertEquals(withAnno.y(), 13);
// WithCDV.toCompositeData adds 1 to x and 2 to y,
// we can check how many time it's been called
// by looking at the values for x and y.
var cdv = mBean5.getCDV();
assertEquals(cdv.x(), 13 /* 12 + 1 */, "x");
assertEquals(cdv.y(), 15 /* 13 + 2 */, "y");
mBean5.setCDV(new WithCDV(14, 15));
cdv = mBean5.getCDV();
assertEquals(cdv.x(), 16 /* 14 + 1*2 */, "x");
assertEquals(cdv.y(), 19 /* 15 + 2*2 */, "y");
// Test non compliant records: this one has an Object (not mappable to OpenType)
var recname4 = new ObjectName("test:type=NCR1");
var x = standard
? expectThrows(IllegalArgumentException.class,
() -> new StandardMBean(new NC1(), NC1MXBean.class, true))
: expectThrows(NotCompliantMBeanException.class,
() -> server.registerMBean(new NC1(), recname4));
reportExpected(x);
assertEquals( originalCause(x).getClass(), OpenDataException.class);
// Test non compliant records: this one has a List<? super Integer>
// (not mappable to OpenType)
var recname5 = new ObjectName("test:type=NCR2");
var x2 = standard
? expectThrows(IllegalArgumentException.class,
() -> new StandardMBean(new NC2(), NC2MXBean.class, true))
: expectThrows(NotCompliantMBeanException.class,
() -> server.registerMBean(new NC2(), recname5));
reportExpected(x2);
assertEquals( originalCause(x2).getClass(), OpenDataException.class);
// Test non compliant records: this one has no getters
// (not mappable to OpenType)
var recname6 = new ObjectName("test:type=NCR3");
var x3 = standard
? expectThrows(IllegalArgumentException.class,
() -> new StandardMBean(new NC3(), NC3MXBean.class, true))
: expectThrows(NotCompliantMBeanException.class,
() -> server.registerMBean(new NC3(), recname6));
reportExpected(x3);
assertEquals( originalCause(x3).getClass(), OpenDataException.class);
// test that a composite data that doesn't have all the records
// components prevents the record from being reconstructed.
var recname7 = new ObjectName("test:type=Records2,instance=6");
Records2 rec2 = new Records2();
var mbean7 = standard
? new StandardMBean(rec2, Records2MXBean.class, true)
: rec2;
server.registerMBean(mbean7, recname7);
var cd7 = (CompositeData) server.getAttribute(recname7, "DataPoint");
var cdt7 = cd7.getCompositeType();
var itemNames7 = List.of("x", "mixed")
.toArray(String[]::new);
var itemDesc7 = Stream.of(itemNames7)
.map(cdt7::getDescription)
.toArray(String[]::new);
var itemTypes7 = Stream.of(itemNames7)
.map(cdt7::getType)
.toArray(OpenType<?>[]::new);
var notmappable = new CompositeType(cdt7.getTypeName(),
cdt7.getDescription(),
itemNames7,
itemDesc7,
itemTypes7);
var itemValues7 = Stream.of(itemNames7)
.map(cd7::get)
.toArray();
var notmappableVal = new CompositeDataSupport(notmappable, itemNames7, itemValues7);
var attribute6 = new Attribute("DataPoint", notmappableVal);
var x4 = expectThrows(MBeanException.class,
standard ? () -> ((StandardMBean)mbean7).setAttribute(attribute6)
: () -> server.setAttribute(recname7, attribute6));
reportExpected(x4);
assertEquals(originalCause(x4).getClass(), InvalidObjectException.class);
}
static final void reportExpected(Throwable x) {
System.out.println("\nGot expected exception: " + x);
Throwable cause = x;
while ((cause = cause.getCause()) != null) {
System.out.println("\tCaused by: " + cause);
}
}
static final Throwable originalCause(Throwable t) {
while (t.getCause() != null) t = t.getCause();
return t;
}
static void println(String name, CompositeData cd) {
var cdt = cd.getCompositeType();
System.out.printf("%s: %s %s\n", name, cdt.getTypeName(),
cdt.keySet().stream()
.map(k -> k + "=" + cd.get(k))
.collect(Collectors.joining(", ", "{ ", " }")));
}
}