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