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}