Gson Class cast exception - java

I have a class to hold some json data as follows:
package org.swx.nursing.tools.configuration.data;
import java.util.Set;
import com.cerner.system.exception.Verifier;
import com.cerner.system.exception.VerifyException;
import com.google.common.collect.ImmutableSet;
/**
* Class representing a simple {#link JsonData#identifier},
* {#link JsonData#data} format. This class can be used to
* persist application data for example in a Configuration file.
*
* #author SW029693
* #since v1.0
*/
public class JsonData <T>{
/**
* Represents a unique identifier
*/
private String identifier;
/**
* Represents the data pertaining to this {#link JsonData#identifier}
*/
private T data;
private static final Set<String> VALID_JSON_ID_TYPES = ImmutableSet.of("CONFIG","HOTKEYS");
public JsonData(String identifier, T data) {
super();
this.identifier = identifier;
this.data = data;
}
/**
* Getter for {#link JsonData#identifier}
* #return
*/
public String getIdentifier() {
return identifier;
}
/**
* Sets the {#link JsonData#identifier} to the given value
* #param identifier
* Represents a unique {#link JsonData#identifier}
* #throws VerifyException
* If the argument is {#code null} or {#code empty}
*/
public void setIdentifier(String identifier) throws VerifyException{
Verifier.verifyNotNull(identifier, "identifier : null");
Verifier.verifyNotEmpty(identifier,"identifier : empty");
this.identifier = identifier;
}
/**
* Getter for {#link JsonData}
* #return
*/
public T getData() {
return data;
}
/**
* Sets the {#link JsonData#data} to the given value
* #param identifier
* Represents a unique {#link JsonData#data}
* #throws VerifyException
* If the argument is {#code null}
*/
public void setData(T data) {
Verifier.verifyNotNull(data, "data : null");
this.data = data;
}
#Override
public String toString() {
return "JsonData [identifier=" + identifier + ", data=" + data + "]";
}
}
I am trying to convert some data to JSON (write it to a file), read it back and cast into the above JsonData object. This is my unit test which fails:
#Test
#SuppressWarnings("unchecked")
public void testWriteContentsToFile() {
ConfigurationManager<JsonData> configurationManager = (ConfigurationManager<JsonData>)
ConfigurationManager.Factory.create();
assertNotNull(configurationManager);
configurationManager.write(getMockJsonData());
System.out.println("1="+getMockJsonData().getData().toString());
assertEquals(getMockJsonData().getData(), ((JsonData) readconfigFile()).getData());
}
The helper methods for this test are as follows:
/**
* Helper method to read the config.json file
* #return
*/
private static JsonData<ConfigurationProperty> readconfigFile() {
Reader reader = null;
JsonData<ConfigurationProperty> data = null;
Gson gson = null;
try {
reader = new FileReader("./config.json");
gson = new GsonBuilder().create();
data = gson.fromJson(reader, JsonData.class);
System.out.println("yooo="+data.getData().getRunnableContext());
} catch (FileNotFoundException e) {
e.printStackTrace();
fail("Test failed while reading the config.json file: "+e.getMessage()); }
finally {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
fail("Test failed while reading the config.json file: "+e.getMessage());
}
}
return data;
}
and
/**
* Helper method which creates a mock {#link JsonData} object for testing purposes
*
* #return An instance of the newly created mock {#link Jsondata} object
*/
private static JsonData<ConfigurationProperty> getMockJsonData() {
JsonData<ConfigurationProperty> data = new JsonData<ConfigurationProperty>("CONFIG",
getMockConfigurationProperty("testKey", "testContext", "APPLICATION"));
System.out.println("data ==="+data);
return data;
}
/**
* Helper method which creates a {#link ConfigurationProperty} based on the given
* non-null and non-empty arguments. This method can be used to get the
* {#link ConfigurationProperty} instance created with the desired fields when ALL
* of the 3 parameters are non-null, non-empty & valid.
*
* #param mockHotKey
* A non-null & non-empty mockHotkey parameter passed in as an argument
*
* #param mockContext
* A non-null & non-empty mockContext parameter passed in as an argument
*
* #param mockContextType
* A non-null & non-empty mockContextType parameter passed in as an argument
*
* #throws VerifierException
* If the argument validation for non-null or non-empty arguments as failed.
*
* #throws IllegalArgumentException
* The {#link ConfigurationProperty.Builder#withRunnableContextType(String type) method
* throws an {#link IllegalArgumentException} if the provided type is not supported in
* {#link ConfigurationProperty#RUNNABLE_CONTEXT_TYPE}
*
* #return An instance of newly created {#link ConfigurationProperty} object
*/
private static ConfigurationProperty getMockConfigurationProperty (
String mockHotKey, String mockContext, String mockContextType) {
Verifier.verifyNotNull(mockHotKey);
Verifier.verifyNotNull(mockContext);
Verifier.verifyNotNull(mockContextType);
Verifier.verifyNotEmpty(mockHotKey);
Verifier.verifyNotEmpty(mockContext);
Verifier.verifyNotEmpty(mockContextType);
return ConfigurationProperty.Builder
.create()
.withHotKey(mockHotKey)
.withRunnableContext(mockContext)
.withRunnableContextType(mockContextType)
.build();
}
I am getting the following error when i run the unit test to read the data from the json file into my JsonData object:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to org.swx.nursing.tools.configuration.data.ConfigurationProperty
at org.swx.nursing.tools.configuration.data.ConfigurationManagerImplTest.readconfigFile(ConfigurationManagerImplTest.java:181)
at org.swx.nursing.tools.configuration.data.ConfigurationManagerImplTest.testWriteContentsToFile(ConfigurationManagerImplTest.java:166)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:66)
at org.jmock.integration.junit4.JMock$1.invoke(JMock.java:37)
at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:105)
at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:86)
at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:94)
at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:84)
at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:49)
at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:98)
at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:61)
at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:54)
at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:34)
at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:44)
at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:52)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:49)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
The file gets written like this:
{
"identifier": "CONFIG",
"data": {
"hotKey": "testKey",
"type": "APPLICATION",
"runnableContext": "testContext"
}
}
I am unable to read the 'data' part of the above json written into the file into the JsonData object due to the above error.
Please advise
Thanks

please see https://sites.google.com/site/gson/gson-user-guide#TOC-Serializing-and-Deserializing-Generic-Types
When you call toJson(obj), Gson calls obj.getClass() to get information on the fields to serialize. Similarly, you can typically pass MyClass.class object in the fromJson(json, MyClass.class) method. This works fine if the object is a non-generic type. However, if the object is of a generic type, then the Generic type information is lost because of Java Type Erasure. Here is an example illustrating the point:
class Foo<T> {
T value;
}
Gson gson = new Gson();
Foo<Bar> foo = new Foo<Bar>();
gson.toJson(foo); // May not serialize foo.value correctly
gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar
The above code fails to interpret value as type Bar because Gson invokes list.getClass() to get its class information, but this method returns a raw class, Foo.class. This means that Gson has no way of knowing that this is an object of type Foo, and not just plain Foo.
You can solve this problem by specifying the correct parameterized type for your generic type. You can do this by using the TypeToken class.
Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
gson.toJson(foo, fooType);
gson.fromJson(json, fooType);
The idiom used to get fooType actually defines an anonymous local inner class containing a method getType() that returns the fully parameterized type.

Related

Is there any way to generate an Avro schema from a hashmap of values?

We are putting all our data points for one row into a hashmap. We don't want to use a pojo because the values are a different set each time. For example, we might get "place" on some records and we might get "hometown" on others. Actually we have thousands of different column names to choose from. Our code looks like this:
Map<String, Object> aMap = new HashMap<>();
aMap.put("id", Integer.valueOf(1));
aMap.put("age", Integer.valueOf(45));
aMap.put("name", "mark");
aMap.put("place", "home");
final GenericRecord record = new GenericData.Record(avroSchema);
aMap.forEach((k, v) -> {
record.put(k, v);
});
writer.write(record);
We would like to put all the values in a map and then generate a schema. Since using the Reflect api, it can be done for a pojo, I was wondering if it could be done from a hashmap as well?
As a side question, Is there any way to eliminate the forEach above and just write the map?
Here is what we came up with. We also had nested columns.
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Parser;
import org.apache.avro.Schema.Type;
/**
* This does NOT do all types. So far just the types we think we need. See
* https://docs.oracle.com/database/nosql-12.1.3.0/GettingStartedGuide/avroschemas.html
* <p>
* We need some error handlling here and when we don't have the correct type, call it out!
* <p>
* This runs in 1-2ms even with a large payload.
*/
public class AvroSchemaBuilder {
/**
* Construct!
*/
private AvroSchemaBuilder() {
//private constructor. All methods are static.
}
/**
* Build the Avro schema and return it.
*
* #param name Name of object.
* #param nameTypeConsumer The nameTypeConsumer of objects being saved.
* #return the Avro schema.
*/
public static Schema getAvroSchema(String name, NameTypeConsumer nameTypeConsumer) {
String json = Lson.toJson(getAvroSchemaAsMap(name, nameTypeConsumer, true));
Parser parser = new Parser().setValidate(true);
return parser.parse(json);
}
/**
* Returns the map with all the attributes to build a schema. This would be recursive if we need
* to build a complex schema. For example for Trends this would build a complex schema where some
* of the types are maps that are themselves described as another nested schema.
*/
private static Map<String, Object> getAvroSchemaAsMap(String name,
NameTypeConsumer nameTypeConsumer,
boolean addNameSpace) {
Map<String, Object> schemaMap = new LinkedHashMap<>();
schemaMap.put("type", "record");
schemaMap.put("name", name);
if (addNameSpace) {
schemaMap.put("namespace", "com.blah.blah");
}
List<Field> fields = new ArrayList();
nameTypeConsumer.consumeNestedNameType((columnName, nestedNameType) -> {
Object avroType;
if (nestedNameType.getNameTypeConsumer() != null) {
avroType = getAvroSchemaAsMap(columnName, nestedNameType.getNameTypeConsumer(), false);
} else {
avroType = getAvroType(nestedNameType.getType()).getName();
}
Object[] types = {"null", avroType}; //adding null first always.
fields.add(new Field(columnName, types));
});
schemaMap.put("fields", fields);
return schemaMap;
}
/**
* Finds the avro type by class.
*
* #param type the Type (this is an avro type).
* #return avro constant.
*/
private static Type getAvroType(Class<?> type) {
if (type.equals(Integer.class)) {
return Type.INT;
}
if (type.equals(Long.class)) {
return Type.LONG;
}
if (type.equals(Float.class)) {
return Type.FLOAT;
}
if (type.equals(Double.class)) {
return Type.DOUBLE;
}
if (type.equals(String.class)) {
return Type.STRING;
}
if (type.equals(Boolean.class)) {
return Type.BOOLEAN;
}
throw new GenericRuntimeException("Cannot get Avro type for type " + type.getName());
}
/**
* Nested class to make our field.
*/
private static class Field {
public final String name;
public final Object[] type;
public Field(String name, Object[] type) {
this.name = name;
this.type = type;
}
}
}

JDBI resultsetmapper create list of objects from query resultset?

Opting for an alternative to JPA and Spring-Data I wanted to try out JDBI for my Repository implementation with SQLite
Repository Code
/**
* SQLite implementation of Foo Repository
*/
public class FooRepository implements FooRepository {
private final DBI connection;
/**
* The constructor initialises the connection to the local SQLite file
*
* #param dataSource jdbc connection string e.g. "jdbc:sqlite::resource:db/foo.db"
* #throws IllegalArgumentException when an invalid DB file is given
*/
public FooRepository(final SQLiteDataSource dataSource) {
checkNotNull(dataSource, "dataSource required");
connection = new DBI(dataSource);
}
/**
* Returns a list of Foo objects for a website locale in the DB
* #return List
* #throws SQLException error querying
*/
#Override
public List<Foo> getFoosByWebsiteLocale(f) throws SQLException {
checkNotNull(websiteLocale, "websiteLocale required");
final String fooQuery = query...
Handle queryHandler = connection.open();
final List<Foo> fooList = queryHandler.createQuery(fooQuery)
.map(FooMapper.class);
queryHandler.close();
return fooList;
}
}
Mapper
public class FooMapper implements ResultSetMapper {
/**
* Construct a Foo object from a record in the result set
* #param index row number
* #param resultRow row
* #param ctx statementcontext
* #return Foo object
* #throws SQLException when accessing sql result set
*/
#Override
public Foo map(final int index, final ResultSet resultRow, final StatementContext ctx) throws SQLException {
return Foo.builder()
.name(resultRow.getString("foo_name"))
.type(resultRow.getString("foo_type"))
.build();
}
}
I am struggling to understand how I will create a list of Foo objects using ResultSetMapper.
The JDBI documentation also appears to be broken on this area :
http://jdbi.org/maven_site/apidocs/org/skife/jdbi/v2/tweak/ResultSetMapper.html
Help would be appreciated on how to make this work.
Your mapper only needs to map one row to one Foo object. JDBI will create the list and put the objects in the list for you.
I.e.:
final List<Foo> fooList = queryHandler.createQuery(fooQuery).map(FooMapper.class).list();

gson - How to include class name property when serializing object of any type

Came to realize I need to include the class name as a property when serializing an object in my application. It would probably be best if I added the class name property for any non-primitive object that is serialized.
I saw that this is a built-in feature in Genson with the useClassMetadata method. But I'm already using gson in my project, so it would be beneficial if I could stick with it.
This is my current attempt:
package com.mycompany.javatest;
import com.google.gson.*;
import java.lang.reflect.*;
public class JavaTest {
public static class GenericSerializer implements JsonSerializer<Object>, JsonDeserializer<Object> {
private static final String CLASS_PROPERTY_NAME = "class";
#Override
public JsonElement serialize(Object src, Type typeOfSrc,
JsonSerializationContext context) {
JsonElement retValue = context.serialize(src);
if (retValue.isJsonObject()) {
retValue.getAsJsonObject().addProperty(CLASS_PROPERTY_NAME, src.getClass().getName());
}
return retValue;
}
#Override
public Object deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
Class actualClass;
if (json.isJsonObject()) {
JsonObject jsonObject = json.getAsJsonObject();
String className = jsonObject.get(CLASS_PROPERTY_NAME).getAsString();
try {
actualClass = Class.forName(className);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
throw new JsonParseException(e.getMessage());
}
}
else {
actualClass = typeOfT.getClass();
}
return context.deserialize(json, actualClass);
}
}
public static class MyClass {
private final String name = "SpongePants SquareBob";
}
public static void main(String[] args) {
MyClass obj = new MyClass();
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapter(Object.class, new GenericSerializer());
Gson gson = gb.create();
System.out.println(gson.toJson(obj, Object.class));
}
}
Prints
{"name":"SpongePants SquareBob"}
I want it to print
{"name":"SpongePants SquareBob","class":"com.mycompany.javatest$MyClass"}
EDIT: Another attempt (this time using GsonFire)
package com.mycompany.javatest;
import com.google.gson.*;
import io.gsonfire.*;
public class JavaTest {
public static class DummyData {
private final String someData = "1337";
}
private static final String CLASS_PROPERTY_NAME = "class";
public static void main(String[] args) {
GsonFireBuilder gfb = new GsonFireBuilder();
gfb.registerPostProcessor(Object.class, new PostProcessor<Object>() {
#Override
public void postDeserialize(Object t, JsonElement je, Gson gson) {
// Ignore
}
#Override
public void postSerialize(JsonElement je, Object t, Gson gson) {
if (je.isJsonObject()) {
je.getAsJsonObject().add(CLASS_PROPERTY_NAME, new JsonPrimitive(t.getClass().getTypeName()));
}
}
});
gfb.registerTypeSelector(Object.class, (JsonElement je) -> {
System.out.println(je);
if (je.isJsonObject()) {
try {
return Class.forName(je.getAsJsonObject().get(CLASS_PROPERTY_NAME).getAsString());
}
catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
return null;
});
Gson gson = gfb.createGson();
DummyData dd = new DummyData();
String json = gson.toJson(dd);
System.out.println(json);
DummyData dd2 = (DummyData) gson.fromJson(json, Object.class); // <-- gives me a ClassCastException
}
}
Yet another answer. It took a bit longer.
Side comment: The above solution would work if you would recursively use reflection to work out the fields in your class. Then serialise those with the special serialiser, while using a separate one for the parent object. This would avoid the stackoverflow.
Having said that - i am a lazy developer, so I like to do things lazy. I am adapting a google solution for you.
NOTE: PLEASE TEST THIS AND ADAPT IT TO YOUR NEEDS. THIS IS A PROTOTYPE AND I HAVE NOT CLEANED UP UNNECESSARY CODE OR CHECKED FOR POSSIBLE ISSUES>
The original source of the code:
https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
So, this is based on the RuntimeTypeAdapterFactory. This factory is provided by google and its purpose is to support hierarchical deserialisation. To do this, you would register a base class and ALL subclasses, with a property that you would like to add as an identifier. If you read the javadocs, this would get much clearer.
This obviously offers us the thing that we want: recursively register different adapters for class types that can handle these, while NOT running in circles and causing a stackoverflow. With one important issue: you have to register ALL subclasses. This is obviously not suitable (though one might argue you could you classpath resolution and simply add all your classes at startup once to be able to use this everywhere). So I looked into the source and changed the code to do this dynamically. Note that google warns against doing it - use it on your own terms :)
Here is my Factory:
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
* <pre> {#code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {#code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {#code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({#code "type"}) and the type labels ({#code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* Create a {#code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {#link #of} factory method. If you don't supply an explicit type
* field name, {#code "type"} will be used. <pre> {#code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
* <pre> {#code
* shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapter.registerSubtype(Circle.class, "Circle");
* shapeAdapter.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {#code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {#code GsonBuilder}, this API supports chaining: <pre> {#code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*/
public final class RuntimeClassNameTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
private RuntimeClassNameTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
}
/**
* Creates a new runtime type adapter using for {#code baseType} using {#code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeClassNameTypeAdapterFactory<T>(baseType, typeFieldName);
}
/**
* Creates a new runtime type adapter for {#code baseType} using {#code "type"} as
* the type field name.
*/
public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeClassNameTypeAdapterFactory<T>(baseType, "class");
}
/**
* Registers {#code type} identified by {#code label}. Labels are case
* sensitive.
*
* #throws IllegalArgumentException if either {#code type} or {#code label}
* have already been registered on this type adapter.
*/
public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {#code type} identified by its {#link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* #throws IllegalArgumentException if either {#code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
// && !String.class.isAssignableFrom(type.getRawType())
if(Object.class.isAssignableFrom(type.getRawType()) ) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
labelToDelegate.put("class", delegate);
subtypeToDelegate.put(type.getRawType(), delegate);
}
// for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
// TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
// labelToDelegate.put(entry.getKey(), delegate);
// subtypeToDelegate.put(entry.getValue(), delegate);
// }
return new TypeAdapter<R>() {
#Override public R read(JsonReader in) throws IOException {
JsonElement jsonElement = Streams.parse(in);
JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
}
String label = labelJsonElement.getAsString();
#SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
}
#Override public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = srcType.getName();
#SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonElement jsonTree = delegate.toJsonTree(value);
if(jsonTree.isJsonPrimitive()) {
Streams.write(jsonTree, out);
} else {
JsonObject jsonObject = jsonTree.getAsJsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
JsonObject clone = new JsonObject();
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
}
}.nullSafe();
}
}
I have added ALL imports for you. This is not (really) published in maven central, though you could find it here: https://mvnrepository.com/artifact/org.danilopianini/gson-extras/0.1.0
Regardless you would have to make adaptions to get this working for you, so I made a copy. The copy fully compiles and you can simply paste it into your code and save yourself the extra dependency.
The important bits of this code are as follows: (and I have purposely left them in but commented out so you can tell)
in create(Gson gson, TypeToken<R> type)
Check if the raw type is assignable from the String class. You want this to be applied to every class object, so this takes care of that. Note the code before that would look up if the type is registered with the class - not needed anymore (accordingly the variables wouldn't be needed; you should clean up the code)
in #Override public void write(JsonWriter out, R value) throws IOException {:
First, we get rid of the label. Our label is and will always be the name of the source type. This is done in:
String label = srcType.getName();
Second, we have to make a distinction between primitive and object types. Primitive types are Strings, Integers etc in the Gson world. This means that our check above (adding an adapter) does not catch the fact that these Object types are in deed primitive types. So we do:
if(jsonTree.isJsonPrimitive()) {
Streams.write(jsonTree, out);
This takes care of that. If it is primitive, just write the tree into the stream. If it is not, we then write all other fields AND the class field into it.
JsonObject clone = new JsonObject();
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
Fewww - finally this now takes care of that. And here is the example to prove my code does what (I believe) you want it to do ;)
public class GsonClassNameTest {
static Gson create = new GsonBuilder().registerTypeAdapterFactory(RuntimeClassNameTypeAdapterFactory.of(Object.class)).create();
public static void main(String[] args) {
String json = create.toJson(new X());
System.out.println(json);
}
public static class X {
public String test = "asd";
public int xyz = 23;
public Y y_class = new Y();
}
public static class Y {
String yTest = "asd2";
Z zTest = new Z();
}
public static class Z {
long longVal = 25;
double doubleTest = 2.4;
}
}
This now outputs this json for you:
{
"class":"google.GsonClassNameTest$X",
"test":"asd",
"xyz":23,
"y_class":{
"class":"google.GsonClassNameTest$Y",
"yTest":"asd2",
"zTest":{
"class":"google.GsonClassNameTest$Z",
"longVal":25,
"doubleTest":2.4
}
}
}
As you can see, Strings, Longs, integers are correctly created. Each class object recursivley got it's classname as well.
This is a generic approach and should work with everything you create. However, if you decide to take this, do me a favour and write a few unit tests ;) Like I mentioned before, I prototyped this implementation.
Hope that gets me a tick :)
Regards,
Artur
Accepted #pandaadb's answer but just wanted to paste the code I'm using. It takes care of serializing with type and de-serializing into the proper subtybe:
package com.mycompany.javatest;
import com.google.gson.*;
import java.lang.reflect.*;
import org.junit.*;
public class JavaTest {
public static class GenericSerializer implements JsonSerializer<Object>, JsonDeserializer<Object> {
private static final String CLASS_PROPERTY_NAME = "class";
private final Gson gson;
public GenericSerializer() {
gson = new Gson();
}
public GenericSerializer(Gson gson) {
this.gson = gson;
}
#Override
public Object deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
Class actualClass;
if (json.isJsonObject()) {
JsonObject jsonObject = json.getAsJsonObject();
String className = jsonObject.get(CLASS_PROPERTY_NAME).getAsString();
try {
actualClass = Class.forName(className);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
throw new JsonParseException(e.getMessage());
}
}
else {
actualClass = typeOfT.getClass();
}
return gson.fromJson(json, actualClass);
}
#Override
public JsonElement serialize(Object src, Type typeOfSrc,
JsonSerializationContext context) {
JsonElement retValue = gson.toJsonTree(src);
if (retValue.isJsonObject()) {
retValue.getAsJsonObject().addProperty(CLASS_PROPERTY_NAME, src.getClass().getName());
}
return retValue;
}
}
public static void main(String[] args) {
GsonBuilder builder = new GsonBuilder();
builder.registerTypeHierarchyAdapter(Object.class, new GenericSerializer());
Gson gson = builder.create();
SomeSuperClass x = new SomeSubClass();
String json = gson.toJson(x);
SomeSuperClass y = gson.fromJson(json, SomeSuperClass.class); // Usually, y would now be of type SomeSuperClass
Assert.assertEquals(x.getClass(), y.getClass()); // y is actually of type SomeSubClass (!)
System.out.println("y.getClass()= " + y.getClass());
}
public static class SomeSuperClass {
}
public static class SomeSubClass extends SomeSuperClass {
private final String someMember = "12345";
}
}
Just tried this myself and this seems to work:
public class GsonClassNameTest {
public static void main(String[] args) {
Gson create = new GsonBuilder().registerTypeHierarchyAdapter(Object.class, new ODeserialiser()).create();
String json = create.toJson(new X());
System.out.println(json);
}
public static class ODeserialiser implements JsonSerializer<Object> {
#Override
public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
Gson gson = new Gson();
JsonElement serialize = gson.toJsonTree(src);
JsonObject o = (JsonObject) serialize;
o.addProperty("class", src.getClass().getName());
return serialize;
}
}
public static class X {
public String test = "asd";
}
}
This prints:
{"test":"asd","class":"google.GsonClassNameTest$X"}
details:
You have to register a Hierarchy adapter, so that if you register it with the Object class, it will be called for any type you pass into it.
You also have to use a different Gson instance within the custom Serializer, otherwise you just keep running in circles and get a Stackoverflow.
Other than that, pretty straight forward :)
Note: I have rather little experience with gson, so there may be a cooler solution to this.
Regards,
Artur
pandaadb's amazing answer wasn't completely working for me since it does not handle arrays/lists and there was a problem with deserialization, so I made a couple of changes:
package org.ctbto.osi.fieldapp.util.gson;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
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 java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* <p>
* Disclaimer: taken from here https://stackoverflow.com/a/40133286/285091 with some modifications
* </p>
*
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
* <pre> {#code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {#code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {#code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({#code "type"}) and the type labels ({#code
* "Rectangle"}) are configurable.
* <p>
* <h3>Registering Types</h3>
* Create a {#code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {#link #of} factory method. If you don't supply an explicit type
* field name, {#code "type"} will be used. <pre> {#code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
* <pre> {#code
* shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapter.registerSubtype(Circle.class, "Circle");
* shapeAdapter.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {#code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {#code GsonBuilder}, this API supports chaining: <pre> {#code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*/
public final class RuntimeClassNameTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
private RuntimeClassNameTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
this.baseType = baseType;
this.typeFieldName = typeFieldName;
}
/**
* Creates a new runtime type adapter using for {#code baseType} using {#code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeClassNameTypeAdapterFactory<T>(baseType, typeFieldName);
}
/**
* Creates a new runtime type adapter for {#code baseType} using {#code "type"} as
* the type field name.
*/
public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeClassNameTypeAdapterFactory<T>(baseType, "class");
}
/**
* Registers {#code type} identified by {#code label}. Labels are case
* sensitive.
*
* #throws IllegalArgumentException if either {#code type} or {#code label}
* have already been registered on this type adapter.
*/
public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
if (type == null || label == null) {
throw new NullPointerException();
}
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {#code type} identified by its {#link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* #throws IllegalArgumentException if either {#code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<Class<?>, TypeAdapter<?>>();
// && !String.class.isAssignableFrom(type.getRawType())
if (Object.class.isAssignableFrom(type.getRawType())) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
labelToDelegate.put(type.getRawType().getName(), delegate);
subtypeToDelegate.put(type.getRawType(), delegate);
}
// for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
// TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
// labelToDelegate.put(entry.getKey(), delegate);
// subtypeToDelegate.put(entry.getValue(), delegate);
// }
return new TypeAdapter<R>() {
#SuppressWarnings("unchecked")
#Override
public R read(JsonReader in) throws IOException {
JsonElement jsonElement = Streams.parse(in);
if (jsonElement.isJsonObject()) {
JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
}
String label = labelJsonElement.getAsString();
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
if (delegate == null) {
Class<R> aClass;
try {
aClass = (Class<R>) Class.forName(label);
} catch (ClassNotFoundException e) {
throw new JsonParseException("Cannot find class " + label, e);
}
TypeToken<R> subClass = TypeToken.get(aClass);
delegate = gson.getDelegateAdapter(RuntimeClassNameTypeAdapterFactory.this, subClass);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype?");
}
}
return delegate.fromJsonTree(jsonElement);
} else if (jsonElement.isJsonNull()) {
return null;
} else {
TypeAdapter<R> delegate = gson.getDelegateAdapter(RuntimeClassNameTypeAdapterFactory.this, type);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
}
}
#Override
public void write(JsonWriter out, R value) throws IOException {
Class<?> srcType = value.getClass();
String label = srcType.getName();
TypeAdapter<R> delegate = getDelegate(srcType);
if (delegate == null) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonElement jsonTree = delegate.toJsonTree(value);
if (!jsonTree.isJsonObject()) {
Streams.write(jsonTree, out);
} else {
JsonObject jsonObject = jsonTree.getAsJsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
JsonObject clone = new JsonObject();
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
}
#SuppressWarnings("unchecked")
private TypeAdapter<R> getDelegate(Class<?> srcType) {
TypeAdapter<?> typeAdapter = subtypeToDelegate.get(srcType);
if (typeAdapter != null) {
return (TypeAdapter<R>) typeAdapter;
}
for (Map.Entry<Class<?>, TypeAdapter<?>> classTypeAdapterEntry : subtypeToDelegate.entrySet()) {
if (classTypeAdapterEntry.getKey().isAssignableFrom(srcType)) {
return (TypeAdapter<R>) classTypeAdapterEntry.getValue();
}
}
return null;
}
}.nullSafe();
}
}
All credit still goes to him/her, though. As s/he says, please test this code before using it!

Java collection like c# KeyedColllection<TKey,TItem>

Is there a Java collection that has the same behavior as the c# abstract KeyedCollection class (that is items can be retrieved by both key and index)? I have looked around but can't find anything similar.
Thanks,
Nick
Here is a full implementation using a "KeyedItem" interface. I've included comments as best I could.
/**
* An interface that must be implemented by an item inserted into a KeyedItemHashMap.
*/
public interface KeyedItem {
/**
* Returns an ID for this item to be used in a KeyedItemHashMap.
* #return An ID for this item to be used in a KeyedItemHashMap.
*/
public String getId();
}
And then the implementation of the HashMap class...
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Hash table based implementation of the {#code Map} interface. This
* implementation provides all of the optional map operations, and permits
* {#code null} values and the {#code null} key. (The {#code HashMap}
* class is roughly equivalent to {#code Hashtable}, except that it is
* unsynchronized and permits nulls.) This class makes no guarantees as to
* the order of the map; in particular, it does not guarantee that the order
* will remain constant over time.
*
* Unlike a typical hash map, this implementation uses KeyedItem's as the
* value. A KeyedItem must implement the {#code KeyedItem} interface and
* provide a unique ID to avoid collisions within the map.
*
* For more details see the {#code HashMap} class.
*/
public final class KeyedItemHashMap implements Map<String,KeyedItem>{
private HashMap<String,KeyedItem> m_map;
/**
* Constructs an empty HashMap with the default initial capacity (16) and
* the default load factor (0.75).
*/
public KeyedItemHashMap() {
m_map = new HashMap<>();
}
/**
* Returns the number of key-value mappings in this map.
* #return The number of key-value mappings in this map.
*/
public int size() {
return m_map.size();
}
/**
* Returns {#code true} if this map contains no key-value mappings.
* #return {#code true} if this map contains no key-value mappings.
*/
public boolean isEmpty() {
return m_map.isEmpty();
}
/**
* Returns {#code true} if this map contains a mapping for the specified key.
* #param key The key whose presence in this map is to be tested.
* #return {#code true} if this map contains a mapping for the specified key.
*/
public boolean containsKey(Object key) {
return m_map.containsKey(key);
}
public boolean containsValue(Object keyedItem) {
return m_map.containsValue(keyedItem);
}
/**
* Returns the string representation of the {#code Object} argument.
*
* #param obj an {#code Object}.
* #return if the argument is {#code null}, then a string equal to
* {#code "null"}; otherwise, the value of
* {#code obj.toString()} is returned.
* #see java.lang.Object#toString()
*/
public KeyedItem get(Object obj) {
return m_map.get(String.valueOf(obj));
}
/**
* Associates the specified value with the keyedItem's ID in this map.
* If the map previously contained a mapping for the keyedItem's ID, the old
* value is replaced.
*
* #param key UNUSED here but necessary for override.
* #param keyedItem Value that implements the KeyedItem interface. The getId() function will
* be used to determine the key for the map.
* #return the previous value associated with {#code keyedItem.getId()}, or
* {#code null} if there was no mapping for {#code keyedItem.getId()}.
* (A {#code null} return can also indicate that the map
* previously associated {#code null} with {#code keyedItem.getId()}.)
*/
public KeyedItem put(String key, KeyedItem keyedItem) {
return m_map.put(keyedItem.getId(), keyedItem);
}
/**
* Associates the specified value with the keyedItem's ID in this map.
* If the map previously contained a mapping for the keyedItem's ID, the old
* value is replaced.
*
* #param keyedItem Value that implements the KeyedItem interface. The getId() function will
* be used to determine the key for the map.
* #return the previous value associated with {#code keyedItem.getId()}, or
* {#code null} if there was no mapping for {#code keyedItem.getId()}.
* (A {#code null} return can also indicate that the map
* previously associated {#code null} with {#code keyedItem.getId()}.)
*/
public KeyedItem put(KeyedItem keyedItem) {
return m_map.put(keyedItem.getId(), keyedItem);
}
/**
* Removes the mapping for the specified keyedItem's ID from this map if present.
*
* #param keyedItem KeyedItem whose mapping is to be removed from the map.
* #return the previous value associated with {#code keyedItem.getId()}, or
* {#code null} if there was no mapping for {#code keyedItem.getId()}.
* (A {#code null} return can also indicate that the map
* previously associated {#code null} with {#code keyedItem.getId()}.)
* #throws ClassCastException if the keyedItem does not implement the KeyedItem interface.
*/
public KeyedItem remove(Object keyedItem) {
return m_map.remove(((KeyedItem)keyedItem).getId());
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* #param map mappings to be stored in this map
* #throws NullPointerException if the specified map is null
*/
public void putAll(Map<? extends String, ? extends KeyedItem> map) {
m_map.putAll(map);
}
/**
* Removes all of the mappings from this map.
* The map will be empty after this call returns.
*/
public void clear() {
m_map.clear();
}
/**
* Returns a {#link Set} view of the keys contained in this map.
* The set is backed by the map, so changes to the map are
* reflected in the set, and vice-versa. If the map is modified
* while an iteration over the set is in progress (except through
* the iterator's own {#code remove} operation), the results of
* the iteration are undefined. The set supports element removal,
* which removes the corresponding mapping from the map, via the
* {#code Iterator.remove}, {#code Set.remove},
* {#code removeAll}, {#code retainAll}, and {#code clear}
* operations. It does not support the {#code add} or {#code addAll}
* operations.
*
* #return a set view of the keys contained in this map
*/
public Set<String> keySet() {
return m_map.keySet();
}
/**
* Returns a {#link Collection} view of the values contained in this map.
* The collection is backed by the map, so changes to the map are
* reflected in the collection, and vice-versa. If the map is
* modified while an iteration over the collection is in progress
* (except through the iterator's own {#code remove} operation),
* the results of the iteration are undefined. The collection
* supports element removal, which removes the corresponding
* mapping from the map, via the {#code Iterator.remove},
* {#code Collection.remove}, {#code removeAll},
* {#code retainAll} and {#code clear} operations. It does not
* support the {#code add} or {#code addAll} operations.
*
* #return a view of the values contained in this map
*/
public Collection<KeyedItem> values() {
return m_map.values();
}
/**
* Returns a {#link Set} view of the mappings contained in this map.
* The set is backed by the map, so changes to the map are
* reflected in the set, and vice-versa. If the map is modified
* while an iteration over the set is in progress (except through
* the iterator's own {#code remove} operation, or through the
* {#code setValue} operation on a map entry returned by the
* iterator) the results of the iteration are undefined. The set
* supports element removal, which removes the corresponding
* mapping from the map, via the {#code Iterator.remove},
* {#code Set.remove}, {#code removeAll}, {#code retainAll} and
* {#code clear} operations. It does not support the
* {#code add} or {#code addAll} operations.
*
* #return a set view of the mappings contained in this map
*/
public Set<Entry<String, KeyedItem>> entrySet() {
return m_map.entrySet();
}
}
This should be flexible enough for you to just add the interface to any class you want to store.
You want to implement the map interface so that your new hashmap can be used in any capacity that a generic hashmap could be used, unfortunately this means you have a put function that takes in a key that isn't used. I've noted that in the comments.
Edit: fixed some syntax errors and included imports
I think you can develop Your own class by extending a HashMap
I think the closest thing I can think of to what you want is a treemap (http://docs.oracle.com/javase/7/docs/api/java/util/TreeMap.html), however you cannot get items by index only by key.
Given an Entity class:
#Data // lombok.Data, to have getters and setters
public class Entity {
private String myIndexingProperty;
// Other fields here
}
An attempt to implement something like the KeyedCollection<T> of C# is (Java 8+):
import com.my.application.model.Entity;
import java.util.*;
import java.util.stream.Collectors;
public final class EntityMap implements Map<String, Entity> {
private TreeSet<Entity> set;
public EntityMap() {
set = new TreeSet<>();
}
#Override
public int size() {
return set.size();
}
#Override
public boolean isEmpty() {
return set.isEmpty();
}
#Override
public boolean containsKey(Object o) {
return set.stream()
.anyMatch(e -> String.valueOf(o).equalsIgnoreCase(e.getMyIndexingProperty()));
}
#Override
public boolean containsValue(Object o) {
return set.stream()
.anyMatch(e -> String.valueOf(o).equalsIgnoreCase(e.getMyIndexingProperty()));
}
#Override
public Entity get(Object o) {
return set.stream()
.filter(e -> String.valueOf(o).equalsIgnoreCase(e.getMyIndexingProperty()))
.findFirst()
.orElse(null);
}
#Override
public Entity put(String s, Entity entity) {
set.add(entity);
return set.stream()
.filter(e -> String.valueOf(entity.getMyIndexingProperty()).equalsIgnoreCase(e.getMyIndexingProperty()))
.findFirst()
.orElse(null);
}
#Override
public Verb remove(Object o) {
boolean removed = set.removeIf(e -> e.equals(o));
if (!removed) {
return null;
}
return get(o);
}
#Override
public void putAll(Map<? extends String, ? extends Entity> map) {
map.forEach(this::put);
}
#Override
public void clear() {
set.clear();
}
#Override
public Set<String> keySet() {
return set.stream()
.map(Entity::getMyIndexingProperty)
.collect(Collectors.toSet());
}
#Override
public Collection<Entity> values() {
return set;
}
#Override
public Set<Entry<String, Entity>> entrySet() {
return set.stream()
.map(e -> new AbstractMap.SimpleEntry<>(e.getMyIndexingProperty(), e))
.collect(Collectors.toSet());
}
}

Dictionary-like data structure. Is this a good practice?

I need a data structure to store different type of objects.E.g. String, Boolean and other classes.
Is using a Map<String, Object> where using the key you get the according object which assumes that you know how to cast it a good practice?
Is there a better solution?
That's a perfect use case for a PropretyHolder I wrote a while ago. You can read in length about it on my blog. I developed it with immutability in mind, feel free to adapt it to your needs.
In general I'd say if you want to profit from type safety in Java you need to know your keys. What I mean by that - it will be hardly possible to develop type safe solution where keys come from external source.
Here's a special key that knows type of its value (it's not complete please download the source for complete version):
public class PropertyKey<T> {
private final Class<T> clazz;
private final String name;
public PropertyKey(Class<T> valueType, String name) {
this.clazz = valueType;
this.name = name;
}
public boolean checkType(Object value) {
if (null == value) {
return true;
}
return this.clazz.isAssignableFrom(value.getClass());
}
... rest of the class
}
Then you develop a data structure that utilizes it:
public class PropertyHolder {
private final ImmutableMap<PropertyKey<?>, ?> storage;
/**
* Returns value for the key of the type extending-the-one-declared-in-the {#link PropertyKey}.
*
* #param key {#link PropertyKey} instance.
* #return Value of the type declared in the key.
*/
#SuppressWarnings("unchecked")
public <T extends Serializable> T get(PropertyKey<T> key) {
return (T) storage.get(key);
}
/**
* Adds key/value pair to the state and returns new
* {#link PropertyHolder} with this state.
*
* #param key {#link PropertyKey} instance.
* #param value Value of type specified in {#link PropertyKey}.
* #return New {#link PropertyHolder} with updated state.
*/
public <T> PropertyHolder put(PropertyKey<T> key, T value) {
Preconditions.checkNotNull(key, "PropertyKey cannot be null");
Preconditions.checkNotNull(value, "Value for key %s is null",
key);
Preconditions.checkArgument(key.checkType(value),
"Property \"%s\" was given "
+ "value of a wrong type \"%s\"", key, value);
// Creates ImmutableMap.Builder with new key/value pair.
return new PropertyHolder(filterOutKey(key)
.put(key, value).build());
}
/**
* Returns {#link Builder} with all the elements from the state except for the given ket.
*
* #param key The key to remove.
* #return {#link Builder} for further processing.
*/
private <T> Builder<PropertyKey<? extends Serializable>, Serializable> filterOutKey(PropertyKey<T> key) {
Builder<PropertyKey<? extends Serializable>, Serializable> builder = ImmutableMap
.<PropertyKey<? extends Serializable>, Serializable> builder();
for (Entry<PropertyKey<? extends Serializable>, Serializable> entry : this.storage.entrySet()) {
if (!entry.getKey().equals(key)) {
builder.put(entry);
}
}
return builder;
}
... rest of the class
}
I omit here a lot of unnecessary details please let me know if something is not clear.
A typesafe heterogeneous container can be used for this purpose:
import java.util.HashMap;
import java.util.Map;
public class Container {
private Map<Class<?>, Object> container = new HashMap<Class<?>, Object>();
public <T> void putElement(Class<T> type, T instance) {
if (type == null) {
throw new NullPointerException("Type is null");
}
//container.put(type, instance); // 'v1'
container.put(type, type.cast(instance)); // 'v2' runtime type safety!
}
public <T> T getElement(Class<T> type) {
return type.cast(container.get(type));
}
public static void main(String[] args) {
Container myCont = new Container();
myCont.putElement(String.class, "aaa");
myCont.putElement(Boolean.class, true);
myCont.putElement(String[].class, new String[] {"one", "two"});
System.out.println(myCont.getElement(String.class));
System.out.println(myCont.getElement(String[].class)[1]);
}
}
Limitation: this container in its form is capable only to store one instance/object type.
In putElement() you can achieve runtime type safety by using a dynamic cast. This will hoewever add an extra overhead.
E.g: Try to pass a raw class object to the container. Note where the exception occurs:
Class raw = Class.forName("MyClass");
myCont.putElement(raw, "aaa"); //ClassCastException if using 'v2'
System.out.println(myCont.getElement(raw)); //ClassCastException if using 'v1'

Categories