/* * This file is part of Skript. * * Skript is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Skript is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Skript. If not, see <http://www.gnu.org/licenses/>. * * * Copyright 2011-2014 Peter Güttinger * */ package ch.njol.skript.registrations; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.NotSerializableException; import java.io.SequenceInputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Chunk; import org.eclipse.jdt.annotation.Nullable; import ch.njol.skript.Skript; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.Converter; import ch.njol.skript.classes.Converter.ConverterInfo; import ch.njol.skript.classes.Parser; import ch.njol.skript.classes.Serializer; import ch.njol.skript.lang.DefaultExpression; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.log.ParseLogHandler; import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.util.StringMode; import ch.njol.skript.variables.DatabaseStorage; import ch.njol.skript.variables.SerializedVariable; import ch.njol.skript.variables.Variables; import ch.njol.util.Kleenean; import ch.njol.util.StringUtils; import ch.njol.yggdrasil.Tag; import ch.njol.yggdrasil.Yggdrasil; import ch.njol.yggdrasil.YggdrasilInputStream; import ch.njol.yggdrasil.YggdrasilOutputStream; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * @author Peter Güttinger */ public abstract class Classes { private Classes() {} @Nullable private static ClassInfo<?>[] classInfos = null; private final static List<ClassInfo<?>> tempClassInfos = new ArrayList<ClassInfo<?>>(); private final static HashMap<Class<?>, ClassInfo<?>> exactClassInfos = new HashMap<Class<?>, ClassInfo<?>>(); private final static HashMap<Class<?>, ClassInfo<?>> superClassInfos = new HashMap<Class<?>, ClassInfo<?>>(); private final static HashMap<String, ClassInfo<?>> classInfosByCodeName = new HashMap<String, ClassInfo<?>>(); /** * @param info info about the class to register */ public static <T> void registerClass(final ClassInfo<T> info) { Skript.checkAcceptRegistrations(); if (classInfosByCodeName.containsKey(info.getCodeName())) throw new IllegalArgumentException("Can't register " + info.getC().getName() + " with the code name " + info.getCodeName() + " because that name is already used by " + classInfosByCodeName.get(info.getCodeName())); if (exactClassInfos.containsKey(info.getC())) throw new IllegalArgumentException("Can't register the class info " + info.getCodeName() + " because the class " + info.getC().getName() + " is already registered"); if (info.getCodeName().length() > DatabaseStorage.MAX_CLASS_CODENAME_LENGTH) throw new IllegalArgumentException("The codename '" + info.getCodeName() + "' is too long to be saved in a database, the maximum length allowed is " + DatabaseStorage.MAX_CLASS_CODENAME_LENGTH); exactClassInfos.put(info.getC(), info); classInfosByCodeName.put(info.getCodeName(), info); tempClassInfos.add(info); } public final static void onRegistrationsStop() { sortClassInfos(); // validate serializeAs for (final ClassInfo<?> ci : getClassInfos()) { if (ci.getSerializeAs() != null) { final ClassInfo<?> sa = getExactClassInfo(ci.getSerializeAs()); if (sa == null) { Skript.error(ci.getCodeName() + "'s 'serializeAs' class is not registered"); } else if (sa.getSerializer() == null) { Skript.error(ci.getCodeName() + "'s 'serializeAs' class is not serializable"); } } } // register to Yggdrasil for (final ClassInfo<?> ci : getClassInfos()) { final Serializer<?> s = ci.getSerializer(); if (s != null) Variables.yggdrasil.registerClassResolver(s); } } /** * Sorts the class infos according to sub/superclasses and relations set with {@link ClassInfo#before(String...)} and {@link ClassInfo#after(String...)}. */ @SuppressFBWarnings("LI_LAZY_INIT_STATIC") private final static void sortClassInfos() { assert classInfos == null; // merge before, after & sub/supertypes in after for (final ClassInfo<?> ci : tempClassInfos) { final Set<String> before = ci.before(); if (before != null && !before.isEmpty()) { for (final ClassInfo<?> ci2 : tempClassInfos) { if (before.contains(ci2.getCodeName())) { ci2.after().add(ci.getCodeName()); before.remove(ci2.getCodeName()); if (before.isEmpty()) break; } } } } for (final ClassInfo<?> ci : tempClassInfos) { for (final ClassInfo<?> ci2 : tempClassInfos) { if (ci == ci2) continue; if (ci.getC().isAssignableFrom(ci2.getC())) ci.after().add(ci2.getCodeName()); } } // remove unresolvable dependencies (and print a warning if testing) for (final ClassInfo<?> ci : tempClassInfos) { final Set<String> s = new HashSet<String>(); final Set<String> before = ci.before(); if (before != null) { for (final String b : before) { if (getClassInfoNoError(b) == null) { s.add(b); } } before.removeAll(s); } for (final String a : ci.after()) { if (getClassInfoNoError(a) == null) { s.add(a); } } ci.after().removeAll(s); if (!s.isEmpty() && Skript.testing()) Skript.warning(s.size() + " dependency/ies could not be resolved for " + ci + ": " + StringUtils.join(s, ", ")); } final List<ClassInfo<?>> classInfos = new ArrayList<ClassInfo<?>>(tempClassInfos.size()); boolean changed = true; while (changed) { changed = false; for (int i = 0; i < tempClassInfos.size(); i++) { final ClassInfo<?> ci = tempClassInfos.get(i); if (ci.after().isEmpty()) { classInfos.add(ci); tempClassInfos.remove(i); i--; for (final ClassInfo<?> ci2 : tempClassInfos) ci2.after().remove(ci.getCodeName()); changed = true; } } } Classes.classInfos = classInfos.toArray(new ClassInfo[classInfos.size()]); // check for circular dependencies if (!tempClassInfos.isEmpty()) { final StringBuilder b = new StringBuilder(); for (final ClassInfo<?> c : tempClassInfos) { if (b.length() != 0) b.append(", "); b.append(c.getCodeName() + " (after: " + StringUtils.join(c.after(), ", ") + ")"); } throw new IllegalStateException("ClassInfos with circular dependencies detected: " + b.toString()); } // debug message if (Skript.debug()) { final StringBuilder b = new StringBuilder(); for (final ClassInfo<?> ci : classInfos) { if (b.length() != 0) b.append(", "); b.append(ci.getCodeName()); } Skript.info("All registered classes in order: " + b.toString()); } } private final static void checkAllowClassInfoInteraction() { if (Skript.isAcceptRegistrations()) throw new IllegalStateException("Cannot use classinfos until registration is over"); } @SuppressWarnings("null") public static List<ClassInfo<?>> getClassInfos() { checkAllowClassInfoInteraction(); final ClassInfo<?>[] ci = classInfos; if (ci == null) return Collections.emptyList(); return Collections.unmodifiableList(Arrays.asList(ci)); } /** * This method can be called even while Skript is loading. * * @param codeName * @return The ClassInfo with the given code name * @throws SkriptAPIException If the given class was not registered */ public static ClassInfo<?> getClassInfo(final String codeName) { final ClassInfo<?> ci = classInfosByCodeName.get(codeName); if (ci == null) throw new SkriptAPIException("No class info found for " + codeName); return ci; } /** * This method can be called even while Skript is loading. * * @param codeName * @return The class info registered with the given code name or null if the code name is invalid or not yet registered */ @Nullable public static ClassInfo<?> getClassInfoNoError(final @Nullable String codeName) { return classInfosByCodeName.get(codeName); } /** * Gets the class info for the given class. * <p> * This method can be called even while Skript is loading. * * @param c The exact class to get the class info for * @return The class info for the given class of null if no info was found. */ @SuppressWarnings("unchecked") @Nullable public static <T> ClassInfo<T> getExactClassInfo(final @Nullable Class<T> c) { return (ClassInfo<T>) exactClassInfos.get(c); } /** * Gets the class info of the given class or its closest registered superclass. This method will never return null unless <tt>c</tt> is null. * * @param c * @return The closest superclass's info */ @SuppressWarnings({"unchecked", "null"}) public static <T> ClassInfo<? super T> getSuperClassInfo(final Class<T> c) { assert c != null; checkAllowClassInfoInteraction(); final ClassInfo<?> i = superClassInfos.get(c); if (i != null) return (ClassInfo<? super T>) i; for (final ClassInfo<?> ci : getClassInfos()) { if (ci.getC().isAssignableFrom(c)) { if (!Skript.isAcceptRegistrations()) superClassInfos.put(c, ci); return (ClassInfo<? super T>) ci; } } assert false; return null; } /** * Gets a class by its code name * * @param codeName * @return the class with the given code name * @throws SkriptAPIException If the given class was not registered */ public static Class<?> getClass(final String codeName) { checkAllowClassInfoInteraction(); return getClassInfo(codeName).getC(); } /** * As the name implies * * @param name * @return the class info or null if the name was not recognised */ @Nullable public static ClassInfo<?> getClassInfoFromUserInput(String name) { checkAllowClassInfoInteraction(); name = "" + name.toLowerCase(); for (final ClassInfo<?> ci : getClassInfos()) { final Pattern[] uip = ci.getUserInputPatterns(); if (uip == null) continue; for (final Pattern pattern : uip) { if (pattern.matcher(name).matches()) return ci; } } return null; } /** * As the name implies * * @param name * @return the class or null if the name was not recognized */ @Nullable public static Class<?> getClassFromUserInput(final String name) { checkAllowClassInfoInteraction(); final ClassInfo<?> ci = getClassInfoFromUserInput(name); return ci == null ? null : ci.getC(); } /** * Gets the default of a class * * @param codeName * @return the expression holding the default value or null if this class doesn't have one * @throws SkriptAPIException If the given class was not registered */ @Nullable public static DefaultExpression<?> getDefaultExpression(final String codeName) { checkAllowClassInfoInteraction(); return getClassInfo(codeName).getDefaultExpression(); } /** * Gets the default expression of a class * * @param c The class * @return The expression holding the default value or null if this class doesn't have one */ @Nullable public static <T> DefaultExpression<T> getDefaultExpression(final Class<T> c) { checkAllowClassInfoInteraction(); final ClassInfo<T> ci = getExactClassInfo(c); return ci == null ? null : ci.getDefaultExpression(); } /** * Gets the name a class was registered with. * * @param c The exact class * @return The name of the class or null if the given class wasn't registered. */ @Nullable public final static String getExactClassName(final Class<?> c) { checkAllowClassInfoInteraction(); final ClassInfo<?> ci = exactClassInfos.get(c); return ci == null ? null : ci.getCodeName(); } /** * Parses without trying to convert anything. * <p> * Can log an error xor other log messages. * * @param s * @param c * @return The parsed object */ @Nullable public static <T> T parseSimple(final String s, final Class<T> c, final ParseContext context) { final ParseLogHandler log = SkriptLogger.startParseLogHandler(); try { for (final ClassInfo<?> info : getClassInfos()) { final Parser<?> parser = info.getParser(); if (parser == null || !parser.canParse(context) || !c.isAssignableFrom(info.getC())) continue; log.clear(); @SuppressWarnings("unchecked") final T t = (T) parser.parse(s, context); if (t != null) { log.printLog(); return t; } } log.printError(); } finally { log.stop(); } return null; } /** * Parses a string to get an object of the desired type. * <p> * Instead of repeatedly calling this with the same class argument, you should get a parser with {@link #getParser(Class)} and use it for parsing. * <p> * Can log an error if it returned null. * * @param s The string to parse * @param c The desired type. The returned value will be of this type or a subclass if it. * @return The parsed object */ @SuppressWarnings({"rawtypes", "unchecked"}) @Nullable public static <T> T parse(final String s, final Class<T> c, final ParseContext context) { final ParseLogHandler log = SkriptLogger.startParseLogHandler(); try { T t = parseSimple(s, c, context); if (t != null) { log.printLog(); return t; } for (final ConverterInfo<?, ?> conv : Converters.getConverters()) { if (context == ParseContext.COMMAND && (conv.options & Converter.NO_COMMAND_ARGUMENTS) != 0) continue; if (c.isAssignableFrom(conv.to)) { log.clear(); final Object o = parseSimple(s, conv.from, context); if (o != null) { t = (T) ((Converter) conv.converter).convert(o); if (t != null) { log.printLog(); return t; } } } } log.printError(); } finally { log.stop(); } return null; } /** * Gets a parser for parsing instances of the desired type from strings. The returned parser may only be used for parsing, i.e. you must not use its toString methods. * * @param to * @return A parser to parse object of the desired type */ @SuppressWarnings("unchecked") @Nullable public final static <T> Parser<? extends T> getParser(final Class<T> to) { checkAllowClassInfoInteraction(); final ClassInfo<?>[] classInfos = Classes.classInfos; if (classInfos == null) return null; for (int i = classInfos.length - 1; i >= 0; i--) { final ClassInfo<?> ci = classInfos[i]; if (to.isAssignableFrom(ci.getC()) && ci.getParser() != null) return (Parser<? extends T>) ci.getParser(); } for (final ConverterInfo<?, ?> conv : Converters.getConverters()) { if (to.isAssignableFrom(conv.to)) { for (int i = classInfos.length - 1; i >= 0; i--) { final ClassInfo<?> ci = classInfos[i]; final Parser<?> parser = ci.getParser(); if (conv.from.isAssignableFrom(ci.getC()) && parser != null) return Classes.createConvertedParser(parser, (Converter<?, ? extends T>) conv.converter); } } } return null; } /** * Gets a parser for an exactly known class. You should usually use {@link #getParser(Class)} instead of this method. * <p> * The main benefit of this method is that it's the only class info method of Skript that can be used while Skript is initializing and thus useful for parsing configs. * * @param c * @return A parser to parse object of the desired type */ @SuppressWarnings("unchecked") @Nullable public final static <T> Parser<? extends T> getExactParser(final Class<T> c) { if (Skript.isAcceptRegistrations()) { for (final ClassInfo<?> ci : tempClassInfos) { if (ci.getC() == c) return (Parser<? extends T>) ci.getParser(); } return null; } else { final ClassInfo<T> ci = getExactClassInfo(c); return ci == null ? null : ci.getParser(); } } private final static <F, T> Parser<T> createConvertedParser(final Parser<?> parser, final Converter<F, T> converter) { return new Parser<T>() { @SuppressWarnings("unchecked") @Override @Nullable public T parse(final String s, final ParseContext context) { final Object f = parser.parse(s, context); if (f == null) return null; return converter.convert((F) f); } @Override public String toString(final T o, final int flags) { throw new UnsupportedOperationException(); } @Override public String toVariableNameString(final T o) { throw new UnsupportedOperationException(); } @Override public String getVariableNamePattern() { throw new UnsupportedOperationException(); } }; } /** * @param o Any object, preferably not an array: use {@link Classes#toString(Object[], boolean)} instead. * @return String representation of the object (using a parser if found or {@link String#valueOf(Object)} otherwise). * @see #toString(Object, StringMode) * @see #toString(Object[], boolean) * @see #toString(Object[], boolean, StringMode) * @see Parser */ public static String toString(final @Nullable Object o) { return toString(o, StringMode.MESSAGE, 0); } public static String getDebugMessage(final @Nullable Object o) { return toString(o, StringMode.DEBUG, 0); } public final static <T> String toString(final @Nullable T o, final StringMode mode) { return toString(o, mode, 0); } private final static <T> String toString(final @Nullable T o, final StringMode mode, final int flags) { assert flags == 0 || mode == StringMode.MESSAGE; if (o == null) return "<none>"; if (o.getClass().isArray()) { if (((Object[]) o).length == 0) return "<none>"; final StringBuilder b = new StringBuilder(); boolean first = true; for (final Object i : (Object[]) o) { if (!first) b.append(", "); b.append(toString(i, mode, flags)); first = false; } return "[" + b.toString() + "]"; } for (final ClassInfo<?> ci : getClassInfos()) { final Parser<?> parser = ci.getParser(); if (parser != null && ci.getC().isInstance(o)) { @SuppressWarnings("unchecked") final String s = mode == StringMode.MESSAGE ? ((Parser<T>) parser).toString(o, flags) : mode == StringMode.DEBUG ? "[" + ci.getCodeName() + ":" + ((Parser<T>) parser).toString(o, mode) + "]" : ((Parser<T>) parser).toString(o, mode); return s; } } return mode == StringMode.VARIABLE_NAME ? "object:" + o : "" + o; } public final static String toString(final Object[] os, final int flags, final boolean and) { return toString(os, and, null, StringMode.MESSAGE, flags); } public final static String toString(final Object[] os, final int flags, final @Nullable ChatColor c) { return toString(os, true, c, StringMode.MESSAGE, flags); } public final static String toString(final Object[] os, final boolean and) { return toString(os, and, null, StringMode.MESSAGE, 0); } public final static String toString(final Object[] os, final boolean and, final StringMode mode) { return toString(os, and, null, mode, 0); } private final static String toString(final Object[] os, final boolean and, final @Nullable ChatColor c, final StringMode mode, final int flags) { if (os.length == 0) return toString(null); if (os.length == 1) return toString(os[0], mode, flags); final StringBuilder b = new StringBuilder(); for (int i = 0; i < os.length; i++) { if (i != 0) { if (c != null) b.append(c.toString()); if (i == os.length - 1) b.append(and ? " and " : " or "); else b.append(", "); } b.append(toString(os[i], mode, flags)); } return "" + b.toString(); } /** * consists of {@link Yggdrasil#MAGIC_NUMBER} and {@link Variables#YGGDRASIL_VERSION} */ private final static byte[] YGGDRASIL_START = {(byte) 'Y', (byte) 'g', (byte) 'g', 0, (Variables.YGGDRASIL_VERSION >>> 8) & 0xFF, Variables.YGGDRASIL_VERSION & 0xFF}; @SuppressWarnings("null") private final static Charset UTF_8 = Charset.forName("UTF-8"); private final static byte[] getYggdrasilStart(final ClassInfo<?> c) throws NotSerializableException { assert Enum.class.isAssignableFrom(Kleenean.class) && Tag.getType(Kleenean.class) == Tag.T_ENUM : Tag.getType(Kleenean.class); // TODO why is this check here? final Tag t = Tag.getType(c.getC()); assert t.isWrapper() || t == Tag.T_STRING || t == Tag.T_OBJECT || t == Tag.T_ENUM; final byte[] cn = t == Tag.T_OBJECT || t == Tag.T_ENUM ? Variables.yggdrasil.getID(c.getC()).getBytes(UTF_8) : null; final byte[] r = new byte[YGGDRASIL_START.length + 1 + (cn == null ? 0 : 1 + cn.length)]; int i = 0; for (; i < YGGDRASIL_START.length; i++) r[i] = YGGDRASIL_START[i]; r[i++] = t.tag; if (cn != null) { r[i++] = (byte) cn.length; for (int j = 0; j < cn.length; j++) r[i++] = cn[j]; } assert i == r.length; return r; } /** * Must be called on the appropriate thread for the given value (i.e. the main thread currently) */ @Nullable public final static SerializedVariable.Value serialize(@Nullable Object o) { if (o == null) return null; // temporary assert Bukkit.isPrimaryThread(); @SuppressWarnings("null") ClassInfo<?> ci = getSuperClassInfo(o.getClass()); if (ci.getSerializeAs() != null) { ci = getExactClassInfo(ci.getSerializeAs()); if (ci == null) { assert false : o.getClass(); return null; } o = Converters.convert(o, ci.getC()); if (o == null) { assert false : ci.getCodeName(); return null; } } final Serializer<?> s = ci.getSerializer(); if (s == null) // value cannot be saved return null; assert s.mustSyncDeserialization() ? Bukkit.isPrimaryThread() : true; try { final ByteArrayOutputStream bout = new ByteArrayOutputStream(); final YggdrasilOutputStream yout = Variables.yggdrasil.newOutputStream(bout); yout.writeObject(o); yout.flush(); yout.close(); final byte[] r = bout.toByteArray(); final byte[] start = getYggdrasilStart(ci); for (int i = 0; i < start.length; i++) assert r[i] == start[i] : o + " (" + ci.getC().getName() + "); " + Arrays.toString(start) + ", " + Arrays.toString(r); final byte[] r2 = new byte[r.length - start.length]; System.arraycopy(r, start.length, r2, 0, r2.length); Object d; assert equals(o, d = deserialize(ci, new ByteArrayInputStream(r2))) : o + " (" + o.getClass() + ") != " + d + " (" + (d == null ? null : d.getClass()) + "): " + Arrays.toString(r); return new SerializedVariable.Value(ci.getCodeName(), r2); } catch (final IOException e) { // shouldn't happen Skript.exception(e); return null; } } private final static boolean equals(final @Nullable Object o, final @Nullable Object d) { if (o instanceof Chunk) { // CraftChunk does neither override equals nor is it a "coordinate-specific singleton" like Block if (!(d instanceof Chunk)) return false; final Chunk c1 = (Chunk) o, c2 = (Chunk) d; return c1.getWorld().equals(c2.getWorld()) && c1.getX() == c2.getX() && c1.getZ() == c2.getZ(); } return o == null ? d == null : o.equals(d); } @Nullable public final static Object deserialize(final ClassInfo<?> type, final byte[] value) { return deserialize(type, new ByteArrayInputStream(value)); } @Nullable public final static Object deserialize(final String type, final byte[] value) { final ClassInfo<?> ci = getClassInfoNoError(type); if (ci == null) return null; return deserialize(ci, new ByteArrayInputStream(value)); } @Nullable public final static Object deserialize(final ClassInfo<?> type, InputStream value) { Serializer<?> s; assert (s = type.getSerializer()) != null && (s.mustSyncDeserialization() ? Bukkit.isPrimaryThread() : true) : type + "; " + s + "; " + Bukkit.isPrimaryThread(); YggdrasilInputStream in = null; try { value = new SequenceInputStream(new ByteArrayInputStream(getYggdrasilStart(type)), value); in = Variables.yggdrasil.newInputStream(value); return in.readObject(); } catch (final IOException e) { // i.e. invalid save if (Skript.testing()) e.printStackTrace(); return null; } finally { if (in != null) { try { in.close(); } catch (final IOException e) {} } try { value.close(); } catch (final IOException e) {} } } /** * Deserialises an object. * <p> * This method must only be called from Bukkits main thread! * * @param type * @param value * @return Deserialised value or null if the input is invalid */ @Deprecated @Nullable public final static Object deserialize(final String type, final String value) { assert Bukkit.isPrimaryThread(); final ClassInfo<?> ci = getClassInfoNoError(type); if (ci == null) return null; final Serializer<?> s = ci.getSerializer(); if (s == null) return null; return s.deserialize(value); } }