Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert ObjectMapper to interface #133

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import java.util.concurrent.ExecutionException;

/**
* Factory for a basic {@link ObjectMapper}.
* Factory for a basic {@link ObjectMapperImpl}.
*/
public class DefaultObjectMapperFactory implements ObjectMapperFactory {
private static final ObjectMapperFactory INSTANCE = new DefaultObjectMapperFactory();
Expand All @@ -35,23 +35,23 @@ public static ObjectMapperFactory getInstance() {
return INSTANCE;
}

private final LoadingCache<Class<?>, ObjectMapper<?>> mapperCache = CacheBuilder.newBuilder()
private final LoadingCache<Class<?>, ObjectMapperImpl<?>> mapperCache = CacheBuilder.newBuilder()
.weakKeys()
.maximumSize(500)
.build(new CacheLoader<Class<?>, ObjectMapper<?>>() {
.build(new CacheLoader<Class<?>, ObjectMapperImpl<?>>() {
@Override
public ObjectMapper<?> load(Class<?> key) throws Exception {
return new ObjectMapper<>(key);
public ObjectMapperImpl<?> load(Class<?> key) throws Exception {
return new ObjectMapperImpl<>(key);
}
});

@NonNull
@Override
@SuppressWarnings("unchecked")
public <T> ObjectMapper<T> getMapper(@NonNull Class<T> type) throws ObjectMappingException {
public <T> ObjectMapperImpl<T> getMapper(@NonNull Class<T> type) throws ObjectMappingException {
Preconditions.checkNotNull(type, "type");
try {
return (ObjectMapper<T>) mapperCache.get(type);
return (ObjectMapperImpl<T>) mapperCache.get(type);
} catch (ExecutionException e) {
if (e.getCause() instanceof ObjectMappingException) {
throw (ObjectMappingException) e.getCause();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*
* <p>Instances of this object should be reached using a {@link GuiceObjectMapperFactory}.</p>
*/
class GuiceObjectMapper<T> extends ObjectMapper<T> {
class GuiceObjectMapper<T> extends ObjectMapperImpl<T> {
private final Injector injector;
private final Key<T> typeKey;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@
import java.util.concurrent.ExecutionException;

/**
* A factory for {@link ObjectMapper}s that will inherit the injector from wherever it is provided.
* A factory for {@link ObjectMapperImpl}s that will inherit the injector from wherever it is provided.
*
* <p>This class is intended to be constructed through Guice dependency injection.</p>
*/
@Singleton
public final class GuiceObjectMapperFactory implements ObjectMapperFactory {
private final LoadingCache<Class<?>, ObjectMapper<?>> cache = CacheBuilder.newBuilder()
private final LoadingCache<Class<?>, ObjectMapperImpl<?>> cache = CacheBuilder.newBuilder()
.weakKeys().maximumSize(512)
.build(new CacheLoader<Class<?>, ObjectMapper<?>>() {
.build(new CacheLoader<Class<?>, ObjectMapperImpl<?>>() {
@Override
public ObjectMapper<?> load(Class<?> key) throws Exception {
public ObjectMapperImpl<?> load(Class<?> key) throws Exception {
return new GuiceObjectMapper<>(injector, key);
}
});
Expand All @@ -53,10 +53,10 @@ protected GuiceObjectMapperFactory(Injector baseInjector) {
@NonNull
@Override
@SuppressWarnings("unchecked")
public <T> ObjectMapper<T> getMapper(@NonNull Class<T> type) throws ObjectMappingException {
public <T> ObjectMapperImpl<T> getMapper(@NonNull Class<T> type) throws ObjectMappingException {
Preconditions.checkNotNull(type, "type");
try {
return (ObjectMapper<T>) cache.get(type);
return (ObjectMapperImpl<T>) cache.get(type);
} catch (ExecutionException e) {
if (e.getCause() instanceof ObjectMappingException) {
throw (ObjectMappingException) e.getCause();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,18 @@
package ninja.leaping.configurate.objectmapping;

import com.google.common.base.Preconditions;
import com.google.common.reflect.TypeToken;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer;
import org.checkerframework.checker.nullness.qual.NonNull;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
* This is the object mapper. It handles conversion between configuration nodes and
* This is the object mapper interface. Its implementation should handle conversion between configuration nodes and
* fields annotated with {@link Setting} in objects.
*
* Values in the node not used by the mapped object will be preserved.
* Values in the node not used by the mapped object should be preserved.
*
* @param <T> The type to work with
*/
public class ObjectMapper<T> {
private final Class<T> clazz;
private final Constructor<T> constructor;
private final Map<String, FieldData> cachedFields = new HashMap<>();

public interface ObjectMapper<T> {

/**
* Create a new object mapper that can work with objects of the given class using the
Expand All @@ -53,7 +40,7 @@ public class ObjectMapper<T> {
* @throws ObjectMappingException If invalid annotated fields are presented
*/
@SuppressWarnings("unchecked")
public static <T> ObjectMapper<T> forClass(@NonNull Class<T> clazz) throws ObjectMappingException {
static <T> ObjectMapper<T> forClass(@NonNull Class<T> clazz) throws ObjectMappingException {
return DefaultObjectMapperFactory.getInstance().getMapper(clazz);
}

Expand All @@ -66,81 +53,42 @@ public static <T> ObjectMapper<T> forClass(@NonNull Class<T> clazz) throws Objec
* @throws ObjectMappingException
*/
@SuppressWarnings("unchecked")
public static <T> ObjectMapper<T>.BoundInstance forObject(@NonNull T obj) throws ObjectMappingException {
static <T> BoundInstance forObject(@NonNull T obj) throws ObjectMappingException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this could be BoundInstance<T>?

Preconditions.checkNotNull(obj);
return forClass((Class<T>) obj.getClass()).bind(obj);
}

/**
* Holder for field-specific information
* Returns whether this object mapper can create new object instances. This may be
* false if the provided class has no zero-argument constructors.
*
* @return Whether new object instances can be created
*/
protected static class FieldData {
private final Field field;
private final TypeToken<?> fieldType;
private final String comment;
boolean canCreateInstances();

public FieldData(Field field, String comment) throws ObjectMappingException {
this.field = field;
this.comment = comment;
this.fieldType = TypeToken.of(field.getGenericType());
}

public void deserializeFrom(Object instance, ConfigurationNode node) throws ObjectMappingException {
TypeSerializer<?> serial = node.getOptions().getSerializers().get(this.fieldType);
if (serial == null) {
throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type "
+ this.fieldType);
}
Object newVal = node.isVirtual() ? null : serial.deserialize(this.fieldType, node);
try {
if (newVal == null) {
Object existingVal = field.get(instance);
if (existingVal != null) {
serializeTo(instance, node);
}
} else {
field.set(instance, newVal);
}
} catch (IllegalAccessException e) {
throw new ObjectMappingException("Unable to deserialize field " + field.getName(), e);
}
}
/**
* Return a view on this mapper that is bound to a single object instance
*
* @param instance The instance to bind to
* @return A view referencing this mapper and the bound instance
*/
BoundInstance<T> bind(T instance);

@SuppressWarnings("rawtypes")
public void serializeTo(Object instance, ConfigurationNode node) throws ObjectMappingException {
try {
Object fieldVal = this.field.get(instance);
if (fieldVal == null) {
node.setValue(null);
} else {
TypeSerializer serial = node.getOptions().getSerializers().get(this.fieldType);
if (serial == null) {
throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type " + this.fieldType);
}
serial.serialize(this.fieldType, fieldVal, node);
}
/**
* Returns a view on this mapper that is bound to a newly created object instance
*
* @see #bind(Object)
* @return Bound mapper attached to a new object instance
* @throws ObjectMappingException If the object could not be constructed correctly
*/
BoundInstance<T> bindToNew() throws ObjectMappingException;

if (node instanceof CommentedConfigurationNode && this.comment != null && !this.comment.isEmpty()) {
CommentedConfigurationNode commentNode = ((CommentedConfigurationNode) node);
if (!commentNode.getComment().isPresent()) {
commentNode.setComment(this.comment);
}
}
} catch (IllegalAccessException e) {
throw new ObjectMappingException("Unable to serialize field " + field.getName(), e);
}
}
}
Class<T> getMappedType();

/**
* Represents an object mapper bound to a certain instance of the object
*/
public class BoundInstance {
private final T boundInstance;

protected BoundInstance(T boundInstance) {
this.boundInstance = boundInstance;
}
interface BoundInstance<T> {

/**
* Populate the annotated fields in a pre-created object
Expand All @@ -149,126 +97,21 @@ protected BoundInstance(T boundInstance) {
* @return The object provided, for easier chaining
* @throws ObjectMappingException If an error occurs while populating data
*/
public T populate(ConfigurationNode source) throws ObjectMappingException {
for (Map.Entry<String, FieldData> ent : cachedFields.entrySet()) {
ConfigurationNode node = source.getNode(ent.getKey());
ent.getValue().deserializeFrom(boundInstance, node);
}
return boundInstance;
}
T populate(ConfigurationNode source) throws ObjectMappingException;

/**
* Serialize the data contained in annotated fields to the configuration node.
*
* @param target The target node to serialize to
* @throws ObjectMappingException if serialization was not possible due to some error.
*/
public void serialize(ConfigurationNode target) throws ObjectMappingException {
for (Map.Entry<String, FieldData> ent : cachedFields.entrySet()) {
ConfigurationNode node = target.getNode(ent.getKey());
ent.getValue().serializeTo(boundInstance, node);
}
}
void serialize(ConfigurationNode target) throws ObjectMappingException;

/**
* Return the instance this mapper is bound to.
*
* @return The active instance
*/
public T getInstance() {
return boundInstance;
}
}

/**
* Create a new object mapper of a given type
*
* @param clazz The type this object mapper will work with
* @throws ObjectMappingException if the provided class is in someway invalid
*/
protected ObjectMapper(Class<T> clazz) throws ObjectMappingException {
this.clazz = clazz;
Constructor<T> constructor = null;
try {
constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
} catch (NoSuchMethodException ignore) {
}
this.constructor = constructor;
Class<? super T> collectClass = clazz;
do {
collectFields(cachedFields, collectClass);
} while (!(collectClass = collectClass.getSuperclass()).equals(Object.class));
}

protected void collectFields(Map<String, FieldData> cachedFields, Class<? super T> clazz) throws ObjectMappingException {
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Setting.class)) {
Setting setting = field.getAnnotation(Setting.class);
String path = setting.value();
if (path.isEmpty()) {
path = field.getName();
}

FieldData data = new FieldData(field, setting.comment());
field.setAccessible(true);
if (!cachedFields.containsKey(path)) {
cachedFields.put(path, data);
}
}
}
}

/**
* Create a new instance of an object of the appropriate type. This method is not
* responsible for any population.
*
* @return The new object instance
* @throws ObjectMappingException If constructing a new instance was not possible
*/
protected T constructObject() throws ObjectMappingException {
if (constructor == null) {
throw new ObjectMappingException("No zero-arg constructor is available for class " + clazz + " but is required to construct new instances!");
}
try {
return constructor.newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new ObjectMappingException("Unable to create instance of target class " + clazz, e);
}
}

/**
* Returns whether this object mapper can create new object instances. This may be
* false if the provided class has no zero-argument constructors.
*
* @return Whether new object instances can be created
*/
public boolean canCreateInstances() {
return constructor != null;
}

/**
* Return a view on this mapper that is bound to a single object instance
*
* @param instance The instance to bind to
* @return A view referencing this mapper and the bound instance
*/
public BoundInstance bind(T instance) {
return new BoundInstance(instance);
}

/**
* Returns a view on this mapper that is bound to a newly created object instance
*
* @see #bind(Object)
* @return Bound mapper attached to a new object instance
* @throws ObjectMappingException If the object could not be constructed correctly
*/
public BoundInstance bindToNew() throws ObjectMappingException {
return new BoundInstance(constructObject());
}

public Class<T> getMappedType() {
return this.clazz;
Object getInstance();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return T.

}
}
Loading