IGNITE-12664 Prevent BinaryObjectExImpl.toString failing with exception if a class of an object serialized with OptimizedMarshaller is not accessible (#7413)
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectExImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectExImpl.java
index e11d8e6..27bbbe7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectExImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/binary/BinaryObjectExImpl.java
@@ -29,6 +29,8 @@
import org.apache.ignite.binary.BinaryObjectException;
import org.apache.ignite.binary.BinaryType;
import org.apache.ignite.internal.binary.builder.BinaryObjectBuilderImpl;
+import org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerInaccessibleClassException;
+import org.apache.ignite.internal.util.typedef.X;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.lang.IgniteUuid;
@@ -228,7 +230,7 @@
buf.a(" [idHash=").a(idHash).a(", hash=").a(hash);
for (String name : meta.fieldNames()) {
- Object val = field(ctx, name);
+ Object val = fieldForToString(ctx, name);
buf.a(", ").a(name).a('=');
@@ -241,6 +243,23 @@
return buf.toString();
}
+ /** */
+ private Object fieldForToString(BinaryReaderHandles ctx, String name) {
+ try {
+ return field(ctx, name);
+ }
+ catch (Exception e) {
+ OptimizedMarshallerInaccessibleClassException e1 =
+ X.cause(e, OptimizedMarshallerInaccessibleClassException.class);
+
+ String msg = "Failed to create a string representation";
+
+ return e1 != null
+ ? "(" + msg + ": class not found " + e1.inaccessibleClass() + ")"
+ : "(" + msg + ")";
+ }
+ }
+
/**
* @param val Value to append.
* @param buf Buffer to append to.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshaller.java b/modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshaller.java
index afcabce..c77cfa25 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshaller.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshaller.java
@@ -227,7 +227,8 @@
return (T)objIn.readObject();
}
catch (ClassNotFoundException e) {
- throw new IgniteCheckedException("Failed to find class with given class loader for unmarshalling " +
+ throw new OptimizedMarshallerInaccessibleClassException(
+ "Failed to find class with given class loader for unmarshalling " +
"(make sure same versions of all classes are available on all nodes or enable peer-class-loading) " +
"[clsLdr=" + clsLdr + ", cls=" + e.getMessage() + "]", e);
}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshallerInaccessibleClassException.java b/modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshallerInaccessibleClassException.java
new file mode 100644
index 0000000..ef8a1cf
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/marshaller/optimized/OptimizedMarshallerInaccessibleClassException.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.internal.marshaller.optimized;
+
+import org.apache.ignite.IgniteCheckedException;
+
+/** */
+public final class OptimizedMarshallerInaccessibleClassException extends IgniteCheckedException {
+ /** */
+ private static final long serialVersionUID = 0L;
+
+ /** */
+ public OptimizedMarshallerInaccessibleClassException(String msg, ClassNotFoundException cause) {
+ super(msg, cause);
+ }
+
+ /** */
+ public String inaccessibleClass() {
+ // Message of ClassNotFoundException is a not found class name
+ return getCause().getMessage();
+ }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectToStringTest.java b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectToStringTest.java
new file mode 100644
index 0000000..a55d393
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/binary/BinaryObjectToStringTest.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.internal.binary;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtField;
+import javassist.CtNewConstructor;
+import javassist.CtNewMethod;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static java.util.Collections.singletonList;
+
+/** */
+public class BinaryObjectToStringTest extends GridCommonAbstractTest {
+ /** */
+ @Test
+ public void testToStringInaccessibleOptimizedMarshallerClass() throws Exception {
+ IgniteEx ign = startGrid(0);
+
+ ign.createCache(new CacheConfiguration<>(DEFAULT_CACHE_NAME));
+
+ assertStringFormContains(new TestContainer(new CustomTestClass()), "CustomTestClass");
+ assertStringFormContains(new TestContainer(new ArrayList<>(singletonList(new CustomTestClass()))),
+ "ArrayList", "CustomTestClass");
+ assertStringFormContains(new TestContainer("abc"), "x=abc");
+ assertStringFormContains(new TestContainer(123), "x=123");
+ assertStringFormContains(new TestIntContainer(123), "i=123");
+
+ assertStringFormContains(new TestContainer(new int[]{1, 2}), "x=[1, 2]");
+ assertStringFormContains(new TestContainer(new Integer[]{1, 2}), "x=[1, 2]");
+ assertStringFormContains(new TestContainer(new ArrayList<>(Arrays.asList(1, 2))), "x=ArrayList {1, 2}");
+ assertStringFormContains(new TestContainer(new HashSet<>(Arrays.asList(1, 2))), "x=HashSet {1, 2}");
+ assertStringFormContains(new TestContainer(new HashMap<>(ImmutableMap.of(1, 2))), "x=HashMap {1=2}");
+
+ ArrayList<Object> nestedList = new ArrayList<>(Arrays.asList(
+ new ArrayList<>(Arrays.asList(1, 2)),
+ new ArrayList<>(Arrays.asList(3, 4))
+ ));
+ assertStringFormContains(new TestContainer(nestedList), "x=ArrayList {ArrayList {1, 2}, ArrayList {3, 4}}");
+
+ assertStringFormMatches(new TestContainer(newExtInstance1()), failedStrPattern("ExternalTestClass1"));
+ assertStringFormMatches(new TestContainer(newExtInstance2()), failedStrPattern("ExternalTestClass2"));
+
+ assertStringFormMatches(new TestContainer(new Object[] {newExtInstance1()}),
+ failedStrPattern("ExternalTestClass1"));
+ assertStringFormMatches(new TestContainer(new ArrayList<>(singletonList(newExtInstance1()))),
+ failedStrPattern("ExternalTestClass1"));
+ assertStringFormMatches(new TestContainer(new HashSet<>(singletonList(newExtInstance1()))),
+ failedStrPattern("ExternalTestClass1"));
+ assertStringFormMatches(new TestContainer(new HashMap<>(ImmutableMap.of(newExtInstance1(), newExtInstance2()))),
+ failedStrPattern("ExternalTestClass1"));
+
+ ArrayList<Object> nestedList2 = new ArrayList<>(singletonList(new ArrayList<>(singletonList(newExtInstance1()))));
+ assertStringFormMatches(new TestContainer(nestedList2), failedStrPattern("ExternalTestClass1"));
+
+ assertStringFormMatches(new TestContainer(new TestExternalizable(newExtInstance1())),
+ failedStrPattern("ExternalTestClass1"));
+ }
+
+ /** */
+ private String failedStrPattern(String className) {
+ return "org.apache.ignite.internal.binary.BinaryObjectToStringTest\\$TestContainer " +
+ "\\[idHash=-?\\d+, hash=-?\\d+, " +
+ "x=\\(Failed to create a string representation: class not found " + className + "\\)]";
+ }
+
+ /** */
+ private static class CustomTestClass {
+ }
+
+ /** */
+ private static class TestExternalizable implements Externalizable {
+ /** */
+ private Object x;
+
+ /** */
+ private TestExternalizable() {
+ }
+
+ /** */
+ private TestExternalizable(Object x) {
+ this.x = x;
+ }
+
+ /** */
+ @Override public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeObject(x);
+ }
+
+ /** */
+ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ x = in.readObject();
+ }
+ }
+
+ /** */
+ private void assertStringFormContains(Object o, String s0, String... ss) {
+ IgniteCache<Object, Object> cache = grid(0).cache(DEFAULT_CACHE_NAME);
+
+ String str = asBinaryObjectString(cache, o);
+
+ assertTrue(str.contains(s0));
+
+ for (String s : ss)
+ assertTrue(str.contains(s));
+ }
+
+ /** */
+ private void assertStringFormMatches(Object o, String pattern) {
+ IgniteCache<Object, Object> cache = grid(0).cache(DEFAULT_CACHE_NAME);
+
+ assertTrue(asBinaryObjectString(cache, o).matches(pattern));
+ }
+
+ /** */
+ private static String asBinaryObjectString(IgniteCache<Object, Object> cache, Object obj) {
+ cache.put(1, obj);
+
+ return cache.withKeepBinary().get(1).toString();
+ }
+
+ /** */
+ private Object newExtInstance1() throws Exception {
+ ClassPool classPool = new ClassPool(ClassPool.getDefault());
+
+ CtClass aClass = classPool.makeClass("ExternalTestClass1");
+ aClass.addInterface(classPool.get("java.io.Externalizable"));
+ aClass.addField(CtField.make("private int x;", aClass));
+ aClass.addConstructor(CtNewConstructor.make("public ExternalTestClass1() {}", aClass));
+ aClass.addConstructor(CtNewConstructor.make("public ExternalTestClass1(int x0) { x = x0; }", aClass));
+ aClass.addMethod(CtNewMethod.make(
+ "public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException { out.writeInt(x); }",
+ aClass));
+ aClass.addMethod(CtNewMethod.make(
+ "public void readExternal(java.io.ObjectInput in) throws java.io.IOException { x = in.readInt(); }",
+ aClass));
+
+ ClassLoader extClsLdr = new ClassLoader() {{
+ byte[] bytecode = aClass.toBytecode();
+
+ defineClass("ExternalTestClass1", bytecode, 0, bytecode.length);
+ }};
+
+ Class<?> extClass = extClsLdr.loadClass("ExternalTestClass1");
+
+ Constructor<?> ctor = extClass.getConstructor(int.class);
+
+ return ctor.newInstance(42);
+ }
+
+ /** */
+ private Object newExtInstance2() throws Exception {
+ ClassPool classPool = new ClassPool(ClassPool.getDefault());
+
+ CtClass aClass = classPool.makeClass("ExternalTestClass2");
+ aClass.addInterface(classPool.get("java.io.Serializable"));
+ aClass.addField(CtField.make("private int x;", aClass));
+ aClass.addConstructor(CtNewConstructor.make("public ExternalTestClass2() {}", aClass));
+ aClass.addConstructor(CtNewConstructor.make("public ExternalTestClass2(int x0) { x = x0; }", aClass));
+ aClass.addMethod(CtNewMethod.make(
+ "private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException { out.writeInt(x); }",
+ aClass));
+ aClass.addMethod(CtNewMethod.make(
+ "private void readObject(java.io.ObjectInputStream in) throws java.io.IOException { x = in.readInt(); }",
+ aClass));
+
+ ClassLoader extClsLdr = new ClassLoader() {{
+ byte[] bytecode = aClass.toBytecode();
+
+ defineClass("ExternalTestClass2", bytecode, 0, bytecode.length);
+ }};
+
+ Class<?> extClass = extClsLdr.loadClass("ExternalTestClass2");
+
+ Constructor<?> ctor = extClass.getConstructor(int.class);
+
+ return ctor.newInstance(42);
+ }
+
+ /** */
+ private static class TestContainer {
+ /** */
+ private final Object x;
+
+ /** */
+ private TestContainer(Object x) {
+ this.x = x;
+ }
+ }
+
+ /** */
+ private static class TestIntContainer {
+ /** */
+ private final int i;
+
+ /** */
+ private TestIntContainer(int i) {
+ this.i = i;
+ }
+ }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java
index 6f818d9..9df13cc 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBinaryObjectsTestSuite.java
@@ -34,6 +34,7 @@
import org.apache.ignite.internal.binary.BinaryObjectBuilderSimpleNameLowerCaseMappersSelfTest;
import org.apache.ignite.internal.binary.BinaryObjectExceptionSelfTest;
import org.apache.ignite.internal.binary.BinaryObjectToStringSelfTest;
+import org.apache.ignite.internal.binary.BinaryObjectToStringTest;
import org.apache.ignite.internal.binary.BinaryObjectTypeCompatibilityTest;
import org.apache.ignite.internal.binary.BinarySerialiedFieldComparatorSelfTest;
import org.apache.ignite.internal.binary.BinarySimpleNameTestPropertySelfTest;
@@ -110,6 +111,7 @@
GridBinaryAffinityKeySelfTest.class,
GridBinaryWildcardsSelfTest.class,
BinaryObjectToStringSelfTest.class,
+ BinaryObjectToStringTest.class,
BinaryObjectTypeCompatibilityTest.class,
// Tests for objects with non-compact footers.