001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.configuration.serialization; 020 021import com.plotsquared.core.configuration.Configuration; 022 023import java.lang.reflect.Constructor; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.lang.reflect.Modifier; 027import java.util.HashMap; 028import java.util.Map; 029import java.util.logging.Level; 030import java.util.logging.Logger; 031 032/** 033 * Utility class for storing and retrieving classes for {@link Configuration}. 034 */ 035public class ConfigurationSerialization { 036 037 public static final String SERIALIZED_TYPE_KEY = "=="; 038 private static final Map<String, Class<? extends ConfigurationSerializable>> aliases = 039 new HashMap<>(); 040 private final Class<? extends ConfigurationSerializable> clazz; 041 042 protected ConfigurationSerialization(Class<? extends ConfigurationSerializable> clazz) { 043 this.clazz = clazz; 044 } 045 046 /** 047 * Attempts to deserialize the given arguments into a new instance of the 048 * given class. 049 * <p>The class must implement {@link ConfigurationSerializable}, including 050 * the extra methods as specified in the javadoc of 051 * ConfigurationSerializable.</p> 052 * <p>If a new instance could not be made, an example being the class not 053 * fully implementing the interface, null will be returned.</p> 054 * 055 * @param args Arguments for deserialization 056 * @param clazz Class to deserialize into 057 * @return New instance of the specified class 058 */ 059 public static ConfigurationSerializable deserializeObject( 060 Map<String, ?> args, 061 Class<? extends ConfigurationSerializable> clazz 062 ) { 063 return new ConfigurationSerialization(clazz).deserialize(args); 064 } 065 066 /** 067 * Attempts to deserialize the given arguments into a new instance of the 068 * given class. 069 * 070 * <p>The class must implement {@link ConfigurationSerializable}, including 071 * the extra methods as specified in the javadoc of 072 * ConfigurationSerializable.</p> 073 * <p>If a new instance could not be made, an example being the class not 074 * fully implementing the interface, null will be returned.</p> 075 * 076 * @param args Arguments for deserialization 077 * @return New instance of the specified class 078 */ 079 public static ConfigurationSerializable deserializeObject(Map<String, ?> args) { 080 Class<? extends ConfigurationSerializable> clazz = null; 081 082 if (args.containsKey(SERIALIZED_TYPE_KEY)) { 083 try { 084 String alias = (String) args.get(SERIALIZED_TYPE_KEY); 085 086 if (alias == null) { 087 throw new IllegalArgumentException("Cannot have null alias"); 088 } 089 clazz = getClassByAlias(alias); 090 if (clazz == null) { 091 throw new IllegalArgumentException( 092 "Specified class does not exist ('" + alias + "')"); 093 } 094 } catch (ClassCastException ex) { 095 ex.fillInStackTrace(); 096 throw ex; 097 } 098 } else { 099 throw new IllegalArgumentException( 100 "Args doesn't contain type key ('" + SERIALIZED_TYPE_KEY + "')"); 101 } 102 103 return new ConfigurationSerialization(clazz).deserialize(args); 104 } 105 106 /** 107 * Registers the given {@link ConfigurationSerializable} class by its 108 * alias. 109 * 110 * @param clazz Class to register 111 */ 112 public static void registerClass(Class<? extends ConfigurationSerializable> clazz) { 113 DelegateDeserialization delegate = clazz.getAnnotation(DelegateDeserialization.class); 114 115 if (delegate == null) { 116 registerClass(clazz, getAlias(clazz)); 117 registerClass(clazz, clazz.getName()); 118 } 119 } 120 121 /** 122 * Registers the given alias to the specified {@link 123 * ConfigurationSerializable} class. 124 * 125 * @param clazz Class to register 126 * @param alias Alias to register as 127 * @see SerializableAs 128 */ 129 public static void registerClass( 130 Class<? extends ConfigurationSerializable> clazz, 131 String alias 132 ) { 133 aliases.put(alias, clazz); 134 } 135 136 /** 137 * Unregisters the specified alias to a {@link ConfigurationSerializable} 138 * 139 * @param alias Alias to unregister 140 */ 141 public static void unregisterClass(String alias) { 142 aliases.remove(alias); 143 } 144 145 /** 146 * Unregisters any aliases for the specified {@link 147 * ConfigurationSerializable} class. 148 * 149 * @param clazz Class to unregister 150 */ 151 public static void unregisterClass(Class<? extends ConfigurationSerializable> clazz) { 152 while (aliases.values().remove(clazz)) { 153 } 154 } 155 156 /** 157 * Attempts to get a registered {@link ConfigurationSerializable} class by 158 * its alias. 159 * 160 * @param alias Alias of the serializable 161 * @return Registered class, or null if not found 162 */ 163 public static Class<? extends ConfigurationSerializable> getClassByAlias(String alias) { 164 return aliases.get(alias); 165 } 166 167 /** 168 * Gets the correct alias for the given {@link ConfigurationSerializable} 169 * class. 170 * 171 * @param clazz Class to get alias for 172 * @return Alias to use for the class 173 */ 174 public static String getAlias(Class<? extends ConfigurationSerializable> clazz) { 175 DelegateDeserialization delegate = clazz.getAnnotation(DelegateDeserialization.class); 176 177 if (delegate != null) { 178 if (delegate.value() == clazz) { 179 delegate = null; 180 } else { 181 return getAlias(delegate.value()); 182 } 183 } 184 185 SerializableAs alias = clazz.getAnnotation(SerializableAs.class); 186 187 if (alias != null) { 188 return alias.value(); 189 } 190 191 return clazz.getName(); 192 } 193 194 protected Method getMethod(String name, boolean isStatic) { 195 try { 196 Method method = this.clazz.getDeclaredMethod(name, Map.class); 197 198 if (!ConfigurationSerializable.class.isAssignableFrom(method.getReturnType())) { 199 return null; 200 } 201 if (Modifier.isStatic(method.getModifiers()) != isStatic) { 202 return null; 203 } 204 205 return method; 206 } catch (NoSuchMethodException | SecurityException ignored) { 207 return null; 208 } 209 } 210 211 protected Constructor<? extends ConfigurationSerializable> getConstructor() { 212 try { 213 return this.clazz.getConstructor(Map.class); 214 } catch (NoSuchMethodException | SecurityException ignored) { 215 return null; 216 } 217 } 218 219 protected ConfigurationSerializable deserializeViaMethod(Method method, Map<String, ?> args) { 220 try { 221 ConfigurationSerializable result = 222 (ConfigurationSerializable) method.invoke(null, args); 223 224 if (result == null) { 225 Logger.getLogger(ConfigurationSerialization.class.getName()).log( 226 Level.SEVERE, 227 "Could not call method '" + method + "' of " + this.clazz 228 + " for deserialization: method returned null" 229 ); 230 } else { 231 return result; 232 } 233 } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException ex) { 234 if (ex instanceof InvocationTargetException) { 235 Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE, 236 "Could not call method '" + method + "' of " + this.clazz 237 + " for deserialization", ex.getCause() 238 ); 239 } else { 240 Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE, 241 "Could not call method '" + method + "' of " + this.clazz 242 + " for deserialization", ex 243 ); 244 } 245 } 246 247 return null; 248 } 249 250 protected ConfigurationSerializable deserializeViaCtor( 251 Constructor<? extends ConfigurationSerializable> ctor, Map<String, ?> args 252 ) { 253 try { 254 return ctor.newInstance(args); 255 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException ex) { 256 if (ex instanceof InvocationTargetException) { 257 Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE, 258 "Could not call constructor '" + ctor + "' of " + this.clazz 259 + " for deserialization", ex.getCause() 260 ); 261 } else { 262 Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE, 263 "Could not call constructor '" + ctor + "' of " + this.clazz 264 + " for deserialization", ex 265 ); 266 } 267 } 268 269 return null; 270 } 271 272 public ConfigurationSerializable deserialize(Map<String, ?> args) { 273 if (args == null) { 274 throw new NullPointerException("Args must not be null"); 275 } 276 ConfigurationSerializable result = null; 277 Method method = getMethod("deserialize", true); 278 if (method != null) { 279 result = deserializeViaMethod(method, args); 280 } 281 if (result == null) { 282 method = getMethod("valueOf", true); 283 if (method != null) { 284 result = deserializeViaMethod(method, args); 285 } 286 } 287 if (result == null) { 288 Constructor<? extends ConfigurationSerializable> constructor = getConstructor(); 289 if (constructor != null) { 290 result = deserializeViaCtor(constructor, args); 291 } 292 } 293 294 return result; 295 } 296 297}