-
-
Notifications
You must be signed in to change notification settings - Fork 68
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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); | ||
} | ||
|
||
|
@@ -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 { | ||
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 | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should return |
||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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>
?