/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.storage.serialize; import com.jcwhatever.nucleus.storage.IDataNode; import com.jcwhatever.nucleus.utils.coords.SyncLocation; import com.jcwhatever.nucleus.managed.reflection.Reflection; import org.bukkit.Location; import org.bukkit.inventory.ItemStack; import java.lang.reflect.Field; import java.lang.reflect.Modifier; /** * Uses reflection to load and save fields from an object into an {@link IDataNode}. * * <p>The fields that can be serialized must be annotated with {@link DataField}.</p> * * <p>Reduces boilerplate code needed to load and save an objects settings at * the cost of performance.</p> * * <p>Only the following field types can be serialized: (Primitive wrappers are not supported)</p> * * <ul> * <li>boolean</li> * <li>byte</li> * <li>short</li> * <li>int</li> * <li>long</li> * <li>float</li> * <li>double</li> * <li>String</li> * <li>instance of {@link Enum}</li> * <li>{@link Location}</li> * <li>{@link ItemStack}</li> * <li>{@link ItemStack[]}</li> * <li>instance of {@link IDataNodeSerializable}</li> * </ul> */ public final class DataFieldSerializer { private DataFieldSerializer() {} /** * Serialize an object with fields annotated with {@link DataField} to a * data node. * * @param object The object to serialize. * @param dataNode The data node to store the objects data fields in. */ public static void serialize(Object object, IDataNode dataNode) { Class<?> clazz = object.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); DataField dataValue = field.getAnnotation(DataField.class); if (dataValue == null) continue; String keyName = dataValue.keyName(); if (keyName.isEmpty()) keyName = field.getName(); Class<?> fieldType = field.getType(); if (!canSerialize(fieldType)) { throw new RuntimeException( "Cannot serialize field type: " + fieldType.getName() + " in class: " + clazz.getName()); } boolean isSet; try { isSet = dataNode.set(keyName, field.get(object)); } catch (IllegalAccessException e) { e.printStackTrace(); throw new AssertionError("Unexpected exception."); } if (!isSet) { throw new RuntimeException( "Failed to serialize field type: " + fieldType.getName() + " in class:" + clazz.getName() + " using IDataNode implementation: " + dataNode.getClass().getName()); } } } /** * Deserialize data field values stored in a data node to the corresponding fields * in the specified object. * * <p>Ignores/skips over fields that do not have a corresponding value in the specified * data node. This includes values that were saved as null.</p> * * @param object The object to deserialize data field values into. * @param dataNode The data node where the values are stored. */ public static void deserializeInto(Object object, IDataNode dataNode) { deserializeInto(object, dataNode, true); } /** * Deserialize data field values stored in a data node to the corresponding fields * in the specified object. * * @param object The object to deserialize data field values into. * @param dataNode The data node where the values are stored. * @param ignoreMissing True to ignore missing/null nodes, false to set the field to * null or primitive default value. */ public static void deserializeInto(Object object, IDataNode dataNode, boolean ignoreMissing) { Class<?> clazz = object.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); DataField dataValue = field.getAnnotation(DataField.class); if (dataValue == null) continue; String keyName = dataValue.keyName(); if (keyName.isEmpty()) keyName = field.getName(); if (ignoreMissing && !dataNode.hasNode(keyName)) continue; if (Modifier.isFinal(field.getModifiers())) Reflection.removeFinal(field); boolean isStatic = Modifier.isStatic(field.getModifiers()); try { setField(field, isStatic ? null : object, keyName, dataNode); } catch (IllegalAccessException e) { e.printStackTrace(); } } } private static void setField(Field field, Object object, String keyName, IDataNode dataNode) throws IllegalAccessException { Class<?> clazz = field.getType(); Object defaultValue = field.get(object); if (clazz.equals(boolean.class)) { field.setBoolean(object, dataNode.getBoolean(keyName)); } else if (clazz.equals(byte.class)) { field.setByte(object, (byte) dataNode.getInteger(keyName)); } else if (clazz.equals(short.class)) { field.setShort(object, (short) dataNode.getInteger(keyName)); } else if (clazz.equals(char.class)) { field.setChar(object, (char) dataNode.getInteger(keyName)); } else if (clazz.equals(int.class)) { field.setInt(object, dataNode.getInteger(keyName)); } else if (clazz.equals(long.class)) { field.setLong(object, dataNode.getLong(keyName)); } else if (clazz.equals(float.class)) { field.setFloat(object, (float) dataNode.getDouble(keyName)); } else if (clazz.equals(double.class)) { field.setDouble(object, dataNode.getDouble(keyName)); } else if (clazz.equals(String.class)) { field.set(object, dataNode.getString(keyName)); } else if (clazz.isEnum()) { //noinspection unchecked field.set(object, dataNode.getEnumGeneric(keyName, null, (Class<? extends Enum<?>>)clazz)); } else if (clazz.equals(Location.class)) { SyncLocation syncLocation = dataNode.getLocation(keyName); field.set(object, syncLocation != null ? syncLocation.getBukkitLocation() : (Location) defaultValue); } else if (clazz.equals(ItemStack.class)) { ItemStack[] stacks = dataNode.getItemStacks(keyName); field.set(object, stacks != null && stacks.length > 0 ? stacks[0] : null); } else if (clazz.equals(ItemStack[].class)) { field.set(object, dataNode.getItemStacks(keyName)); } else if (IDataNodeSerializable.class.isAssignableFrom(clazz)) { //noinspection unchecked Object result = dataNode.getSerializable(keyName, (Class<? extends IDataNodeSerializable>)clazz); field.set(object, result); } } private static boolean canSerialize(Class<?> clazz) { return clazz.equals(boolean.class) || clazz.equals(byte.class) || clazz.equals(char.class) || clazz.equals(short.class) || clazz.equals(int.class) || clazz.equals(long.class) || clazz.equals(float.class) || clazz.equals(double.class) || clazz.isEnum() || clazz.equals(String.class) || clazz.equals(Location.class) || clazz.equals(ItemStack.class) || clazz.equals(ItemStack[].class) || IDataNodeSerializable.class.isAssignableFrom(clazz); } }