Merge PR #3403 by @eviltak - serialization work

develop
Cervator 2018-07-15 12:35:11 -04:00
commit 639752cd95
12 changed files with 588 additions and 6 deletions

View File

@ -0,0 +1,85 @@
/*
* Copyright 2018 MovingBlocks
*
* 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 org.terasology.persistence.typeHandling.gson;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.junit.Assert;
import org.junit.Test;
import org.terasology.persistence.typeHandling.extensionTypes.ColorTypeHandler;
import org.terasology.rendering.nui.Color;
import java.util.Objects;
public class GsonTypeHandlerAdapterTest {
private static final String OBJECT_JSON_ARRAY = "{\"color\":[222,173,190,239],\"i\":-123}";
private static final String OBJECT_JSON_HEX = "{\"color\":DEADBEEF,\"i\":-123}";
private static final TestClass OBJECT = new TestClass(new Color(0xDEADBEEF), -123);
private final Gson gson = GsonFactory.createGsonWithTypeHandlers(
TypeHandlerEntry.of(Color.class, new ColorTypeHandler())
);
/**
* {@link GsonTypeHandlerAdapter#read(JsonReader)} is tested by deserializing an object from JSON
* via Gson with a registered {@link GsonTypeHandlerAdapterFactory} which creates instances of
* {@link GsonTypeHandlerAdapter}.
*/
@Test
public void testRead() {
// Deserialize object with color as JSON array
TestClass deserializedObject = gson.fromJson(OBJECT_JSON_ARRAY, TestClass.class);
Assert.assertEquals(OBJECT, deserializedObject);
// Deserialize object with color as hex string
deserializedObject = gson.fromJson(OBJECT_JSON_HEX, TestClass.class);
Assert.assertEquals(OBJECT, deserializedObject);
}
/**
* {@link GsonTypeHandlerAdapter#write(JsonWriter, Object)} is tested by serializing an object to JSON
* via Gson with a registered {@link GsonTypeHandlerAdapterFactory} which creates instances of
* {@link GsonTypeHandlerAdapter}.
*/
@Test
public void testWrite() {
String serializedObject = gson.toJson(OBJECT);
Assert.assertEquals(OBJECT_JSON_ARRAY, serializedObject);
}
private static class TestClass {
private final Color color;
private final int i;
private TestClass(Color color, int i) {
this.color = color;
this.i = i;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestClass testClass = (TestClass) o;
return i == testClass.i &&
Objects.equals(color, testClass.color);
}
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2017 MovingBlocks
*
* 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 org.terasology.persistence.typeHandling.gson;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import org.junit.Assert;
import org.junit.Test;
import org.terasology.math.geom.Rect2i;
import org.terasology.math.geom.Vector4f;
import org.terasology.persistence.typeHandling.TypeSerializationLibrary;
import org.terasology.reflection.copy.CopyStrategyLibrary;
import org.terasology.reflection.reflect.ReflectionReflectFactory;
import org.terasology.rendering.nui.Color;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class GsonTypeSerializationLibraryAdapterFactoryTest {
private static final TestClass OBJECT = new TestClass(
new Color(0xDEADBEEF),
ImmutableSet.of(Vector4f.zero(), Vector4f.one()),
ImmutableMap.of(
"someRect",
Rect2i.createFromMinAndSize(-3, -3, 10, 10)
),
ImmutableMap.of(0, 1, 1, 0),
-0xDECAF
);
private static final String OBJECT_JSON = "{\"color\":[222,173,190,239],\"vector4fs\":[[0.0,0.0,0.0,0.0]," +
"[1.0,1.0,1.0,1.0]],\"rect2iMap\":{\"someRect\":{\"min\":[-3,-3],\"size\":[10,10]}},\"i\":-912559}";
private final ReflectionReflectFactory reflectFactory = new ReflectionReflectFactory();
private final CopyStrategyLibrary copyStrategyLibrary = new CopyStrategyLibrary(reflectFactory);
private final TypeSerializationLibrary typeSerializationLibrary =
TypeSerializationLibrary.createDefaultLibrary(reflectFactory, copyStrategyLibrary);
private final Gson gson = GsonFactory.createGsonWithTypeSerializationLibrary(typeSerializationLibrary);
@Test
public void testSerialize() {
String serializedObject = gson.toJson(OBJECT);
Assert.assertEquals(OBJECT_JSON, serializedObject);
}
@Test
public void testDeserialize() {
TestClass deserializedObject = gson.fromJson(OBJECT_JSON, TestClass.class);
Assert.assertEquals(OBJECT, deserializedObject);
}
private static class TestClass {
private final Color color;
private final Set<Vector4f> vector4fs;
private final Map<String, Rect2i> rect2iMap;
// Will not be serialized
private final Map<Integer, Integer> intMap;
private final int i;
private TestClass(Color color, Set<Vector4f> vector4fs, Map<String, Rect2i> rect2iMap,
Map<Integer, Integer> intMap, int i) {
this.color = color;
this.vector4fs = vector4fs;
this.rect2iMap = rect2iMap;
this.intMap = intMap;
this.i = i;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestClass testClass = (TestClass) o;
return i == testClass.i &&
Objects.equals(color, testClass.color) &&
Objects.equals(vector4fs, testClass.vector4fs) &&
Objects.equals(rect2iMap, testClass.rect2iMap);
}
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2018 MovingBlocks
*
* 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 org.terasology.persistence.typeHandling.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapterFactory;
import org.terasology.persistence.typeHandling.TypeSerializationLibrary;
/**
* Class containing static factory methods for generating {@link Gson} objects that follow Terasology
* serialization rules and support Terasology TypeHandlers.
*/
public class GsonFactory {
/**
* Create a {@link GsonBuilder} with options set to comply with Terasology JSON serialization rules.
*/
public static GsonBuilder createDefaultGsonBuilder() {
return new GsonBuilder()
.setExclusionStrategies(new GsonMapExclusionStrategy());
}
/**
* Create a {@link Gson} object which uses type handlers loaded from the given
* {@link TypeSerializationLibrary} and complies with Terasology JSON serialization rules.
*
* @param typeSerializationLibrary The {@link TypeSerializationLibrary} to load type handler
* definitions from
*/
public static Gson createGsonWithTypeSerializationLibrary(TypeSerializationLibrary typeSerializationLibrary) {
TypeAdapterFactory typeAdapterFactory =
new GsonTypeSerializationLibraryAdapterFactory(typeSerializationLibrary);
return createDefaultGsonBuilder()
.registerTypeAdapterFactory(typeAdapterFactory)
.create();
}
/**
* Create a {@link Gson} object which uses the given type handlers and complies with Terasology
* JSON serialization rules.
*
* @param typeHandlerEntries The type handlers to use during serialization.
*/
@SuppressWarnings("unchecked")
public static Gson createGsonWithTypeHandlers(TypeHandlerEntry<?>... typeHandlerEntries) {
GsonTypeHandlerAdapterFactory typeAdapterFactory = new GsonTypeHandlerAdapterFactory();
for (TypeHandlerEntry typeHandlerEntry : typeHandlerEntries) {
typeAdapterFactory.addTypeHandler(typeHandlerEntry);
}
return createDefaultGsonBuilder()
.registerTypeAdapterFactory(typeAdapterFactory)
.create();
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2017 MovingBlocks
*
* 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 org.terasology.persistence.typeHandling.gson;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import org.terasology.utilities.ReflectionUtil;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
public class GsonMapExclusionStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipField(FieldAttributes f) {
Type fieldType = f.getDeclaredType();
Class<?> fieldClass = ReflectionUtil.getClassOfType(fieldType);
if (Map.class.isAssignableFrom(fieldClass)) {
Type mapKeyType = ReflectionUtil.getTypeParameter(fieldType, 0);
return String.class != mapKeyType;
}
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2018 MovingBlocks
* Copyright (C) 2011 Google Inc.
*
* 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.
*
* GsonTypeHandlerAdapter is a less featureful clone of the package-private
* class TreeTypeAdapter in Gson 2.6.2.
*/
package org.terasology.persistence.typeHandling.gson;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.terasology.persistence.typeHandling.TypeHandler;
import org.terasology.utilities.ReflectionUtil;
import java.io.IOException;
/**
* Adapts a {@link TypeHandler} as a Gson {@link TypeAdapter}. Instances of {@link GsonTypeHandlerAdapter},
* when registered as type adapters in a {@link Gson} object, can be used to (de)serialize objects
* to JSON (via Gson) with the rules specified by the {@link GsonTypeHandlerAdapter#typeHandler}.
*
* Since instances of {@link GsonTypeHandlerAdapter} require a {@link Gson} object and a
* {@link TypeToken}, it is recommended to register {@link GsonTypeHandlerAdapter} type adapters as a
* type adapter factory via a {@link com.google.gson.TypeAdapterFactory} like
* {@link GsonTypeHandlerAdapterFactory}.
*/
public final class GsonTypeHandlerAdapter<T> extends TypeAdapter<T> {
private final TypeHandler<T> typeHandler;
private final JsonSerializer<T> serializer;
private final JsonDeserializer<T> deserializer;
private final Gson gson;
private final TypeToken<T> typeToken;
GsonTypeHandlerAdapter(TypeHandler<T> typeHandler,
Gson gson, TypeToken<T> typeToken) {
this.typeHandler = typeHandler;
this.serializer = (src, typeOfSrc, context) ->
((GsonPersistedData) typeHandler.serialize(src, new GsonSerializationContext(context)))
.getElement();
this.deserializer = (json, typeOfT, context) ->
typeHandler.deserialize(new GsonPersistedData(json), new GsonDeserializationContext(context));
this.gson = gson;
this.typeToken = typeToken;
}
@Override
public T read(JsonReader in) throws IOException {
JsonElement value = Streams.parse(in);
if (value.isJsonNull()) {
return null;
}
return deserializer.deserialize(value, typeToken.getType(), (JsonDeserializationContext) ReflectionUtil.readField(gson, "deserializationContext"));
}
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
JsonElement tree = serializer.serialize(value, typeToken.getType(), (JsonSerializationContext) ReflectionUtil.readField(gson, "serializationContext"));
Streams.write(tree, out);
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2018 MovingBlocks
*
* 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 org.terasology.persistence.typeHandling.gson;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import org.terasology.persistence.typeHandling.TypeHandler;
import org.terasology.persistence.typeHandling.TypeSerializationLibrary;
import java.util.HashMap;
import java.util.Map;
/**
* A Gson {@link TypeAdapterFactory} that creates a {@link GsonTypeHandlerAdapter} for each
* {@link TypeHandler} registered in the {@link #typeHandlerMap}.
*/
public class GsonTypeHandlerAdapterFactory implements TypeAdapterFactory {
private Map<Class<?>, TypeHandler<?>> typeHandlerMap;
public GsonTypeHandlerAdapterFactory() {
typeHandlerMap = new HashMap<>();
}
/**
* Adds a {@link TypeHandler} to the {@link #typeHandlerMap} for the given type.
*
* @param typeHandlerEntry The {@link TypeHandlerEntry} encapsulating the {@link TypeHandler} for
* the given type.
*/
public <T> void addTypeHandler(TypeHandlerEntry<T> typeHandlerEntry) {
addTypeHandler(typeHandlerEntry.type, typeHandlerEntry.typeHandler);
}
/**
* Adds a {@link TypeHandler} to the {@link #typeHandlerMap} for the given type.
* @param type The {@link Class} of the type.
* @param typeHandler The {@link TypeHandler} for the type.
*/
public <T> void addTypeHandler(Class<T> type, TypeHandler<T> typeHandler) {
typeHandlerMap.put(type, typeHandler);
}
/**
* Returns a boolean stating whether the {@link #typeHandlerMap} contains a type handler for the given type.
* @param type The {@link Class} of the given type.
*/
public boolean containsTypeHandlerFor(Class<?> type) {
return typeHandlerMap.containsKey(type);
}
@SuppressWarnings("unchecked")
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<? super T> rawType = type.getRawType();
if (!containsTypeHandlerFor(rawType)) {
return null;
}
return new GsonTypeHandlerAdapter<>((TypeHandler<T>) typeHandlerMap.get(rawType), gson, type);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2018 MovingBlocks
*
* 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 org.terasology.persistence.typeHandling.gson;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import org.terasology.persistence.typeHandling.TypeHandler;
import org.terasology.persistence.typeHandling.TypeSerializationLibrary;
import java.lang.reflect.Type;
/**
* A Gson {@link TypeAdapterFactory} that dynamically looks up the {@link TypeHandler} from a
* {@link TypeSerializationLibrary} for each type encountered, and creates a {@link GsonTypeHandlerAdapter} with
* the retrieved {@link TypeHandler}.
*/
public class GsonTypeSerializationLibraryAdapterFactory implements TypeAdapterFactory {
private final TypeSerializationLibrary typeSerializationLibrary;
public GsonTypeSerializationLibraryAdapterFactory(TypeSerializationLibrary typeSerializationLibrary) {
this.typeSerializationLibrary = typeSerializationLibrary;
}
@SuppressWarnings("unchecked")
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Type rawType = type.getType();
TypeHandler<T> typeHandler = (TypeHandler<T>) typeSerializationLibrary.getHandlerFor(rawType);
if (typeHandler == null) {
return null;
}
return new GsonTypeHandlerAdapter<>(typeHandler, gson, type);
}
}

View File

@ -15,6 +15,7 @@
*/
package org.terasology.persistence.typeHandling.gson;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
@ -26,12 +27,16 @@ import org.terasology.persistence.typeHandling.TypeHandler;
import java.lang.reflect.Type;
/**
* Adapts a {@link TypeHandler} as a legacy Gson {@link JsonSerializer} and {@link JsonDeserializer}.
* Instances of {@link LegacyGsonTypeHandlerAdapter}, when registered as type adapters in a {@link Gson}
* object, can be used to (de)serialize objects to JSON (via Gson) with the rules specified by
* the {@link #typeHandler}.
*/
public class JsonTypeHandlerAdapter<T> implements JsonDeserializer<T>, JsonSerializer<T> {
public class LegacyGsonTypeHandlerAdapter<T> implements JsonDeserializer<T>, JsonSerializer<T> {
private TypeHandler<T> typeHandler;
public JsonTypeHandlerAdapter(TypeHandler<T> handler) {
public LegacyGsonTypeHandlerAdapter(TypeHandler<T> handler) {
this.typeHandler = handler;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2018 MovingBlocks
*
* 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 org.terasology.persistence.typeHandling.gson;
import org.terasology.persistence.typeHandling.TypeHandler;
/**
* A class containing a {@link TypeHandler} and the {@link Class} of the type it handles.
*
* @param <T> The type handled by the {@link TypeHandler} contained in the {@link TypeHandlerEntry}.
*/
public class TypeHandlerEntry<T> {
public final Class<T> type;
public final TypeHandler<T> typeHandler;
private TypeHandlerEntry(Class<T> type, TypeHandler<T> typeHandler) {
this.type = type;
this.typeHandler = typeHandler;
}
/**
* Creates a {@link TypeHandlerEntry} for a {@link TypeHandler} of the type {@link U}.
*/
public static <U> TypeHandlerEntry<U> of(Class<U> type, TypeHandler<U> typeHandler) {
return new TypeHandlerEntry<>(type, typeHandler);
}
}

View File

@ -39,7 +39,7 @@ import org.terasology.math.Border;
import org.terasology.persistence.ModuleContext;
import org.terasology.persistence.typeHandling.TypeSerializationLibrary;
import org.terasology.persistence.typeHandling.extensionTypes.AssetTypeHandler;
import org.terasology.persistence.typeHandling.gson.JsonTypeHandlerAdapter;
import org.terasology.persistence.typeHandling.gson.LegacyGsonTypeHandlerAdapter;
import org.terasology.persistence.typeHandling.mathTypes.BorderTypeHandler;
import org.terasology.reflection.metadata.ClassMetadata;
import org.terasology.reflection.metadata.FieldMetadata;
@ -106,7 +106,7 @@ public class UIFormat extends AbstractAssetFileFormat<UIData> {
.registerTypeAdapter(UIData.class, new UIDataTypeAdapter())
.registerTypeHierarchyAdapter(UIWidget.class, new UIWidgetTypeAdapter(nuiManager));
for (Class<?> handledType : library.getCoreTypes()) {
gsonBuilder.registerTypeAdapter(handledType, new JsonTypeHandlerAdapter<>(library.getHandlerFor(handledType)));
gsonBuilder.registerTypeAdapter(handledType, new LegacyGsonTypeHandlerAdapter<>(library.getHandlerFor(handledType)));
}
// override the String TypeAdapter from the serialization library

View File

@ -31,7 +31,7 @@ import org.terasology.assets.format.AssetDataFile;
import org.terasology.assets.module.annotations.RegisterAssetFileFormat;
import org.terasology.persistence.ModuleContext;
import org.terasology.persistence.typeHandling.extensionTypes.ColorTypeHandler;
import org.terasology.persistence.typeHandling.gson.JsonTypeHandlerAdapter;
import org.terasology.persistence.typeHandling.gson.LegacyGsonTypeHandlerAdapter;
import org.terasology.reflection.metadata.ClassLibrary;
import org.terasology.reflection.metadata.ClassMetadata;
import org.terasology.registry.CoreRegistry;
@ -66,7 +66,7 @@ public class UISkinFormat extends AbstractAssetFileFormat<UISkinData> {
.registerTypeAdapter(Font.class, new AssetTypeAdapter<>(Font.class))
.registerTypeAdapter(UISkinData.class, new UISkinTypeAdapter())
.registerTypeAdapter(TextureRegion.class, new TextureRegionTypeAdapter())
.registerTypeAdapter(Color.class, new JsonTypeHandlerAdapter<>(new ColorTypeHandler()))
.registerTypeAdapter(Color.class, new LegacyGsonTypeHandlerAdapter<>(new ColorTypeHandler()))
.registerTypeAdapter(Optional.class, new OptionalTextureRegionTypeAdapter())
.create();
}

View File

@ -199,4 +199,21 @@ public final class ReflectionUtil {
return getTypeParameterForSuperInterface(targetClass.getGenericSuperclass(), superClass, index);
}
public static Object readField(Object object, String fieldName) {
Class<?> cls = object.getClass();
for (Class<?> c = cls; c != null; c = c.getSuperclass()) {
try {
final Field field = c.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
} catch (final NoSuchFieldException e) {
// Try parent
} catch (Exception e) {
throw new IllegalArgumentException(
"Cannot access field " + cls.getName() + "." + fieldName, e);
}
}
throw new IllegalArgumentException(
"Cannot find field " + cls.getName() + "." + fieldName);
}
}