diff --git a/paper-api/src/main/java/org/bukkit/configuration/file/FileConfiguration.java b/paper-api/src/main/java/org/bukkit/configuration/file/FileConfiguration.java index 3f9992e76b..9d6d1c6132 100644 --- a/paper-api/src/main/java/org/bukkit/configuration/file/FileConfiguration.java +++ b/paper-api/src/main/java/org/bukkit/configuration/file/FileConfiguration.java @@ -1,25 +1,68 @@ package org.bukkit.configuration.file; +import com.google.common.base.Charsets; import com.google.common.io.Files; import org.apache.commons.lang.Validate; import org.bukkit.configuration.InvalidConfigurationException; + import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileWriter; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.Charset; + import org.bukkit.configuration.Configuration; import org.bukkit.configuration.MemoryConfiguration; +import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; /** * This is a base class for all File based implementations of {@link * Configuration} */ public abstract class FileConfiguration extends MemoryConfiguration { + /** + * This value specified that the system default encoding should be + * completely ignored, as it cannot handle the ASCII character set, or it + * is a strict-subset of UTF8 already (plain ASCII). + * + * @deprecated temporary compatibility measure + */ + @Deprecated + public static final boolean UTF8_OVERRIDE; + /** + * This value specifies if the system default encoding is unicode, but + * cannot parse standard ASCII. + * + * @deprecated temporary compatibility measure + */ + @Deprecated + public static final boolean UTF_BIG; + /** + * This value specifies if the system supports unicode. + * + * @deprecated temporary compatibility measure + */ + @Deprecated + public static final boolean SYSTEM_UTF; + static { + final byte[] testBytes = Base64Coder.decode("ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX4NCg=="); + final String testString = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\r\n"; + final Charset defaultCharset = Charset.defaultCharset(); + final String resultString = new String(testBytes, defaultCharset); + final boolean trueUTF = defaultCharset.name().contains("UTF"); + UTF8_OVERRIDE = !testString.equals(resultString) || defaultCharset.equals(Charset.forName("US-ASCII")); + SYSTEM_UTF = trueUTF || UTF8_OVERRIDE; + UTF_BIG = trueUTF && UTF8_OVERRIDE; + } + /** * Creates an empty {@link FileConfiguration} with no default values. */ @@ -43,6 +86,9 @@ public abstract class FileConfiguration extends MemoryConfiguration { * If the file does not exist, it will be created. If already exists, it * will be overwritten. If it cannot be overwritten or created, an * exception will be thrown. + *

+ * This method will save using the system default encoding, or possibly + * using UTF8. * * @param file File to save to. * @throws IOException Thrown when the given file cannot be written to for @@ -56,7 +102,7 @@ public abstract class FileConfiguration extends MemoryConfiguration { String data = saveToString(); - FileWriter writer = new FileWriter(file); + Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF8_OVERRIDE && !UTF_BIG ? Charsets.UTF_8 : Charset.defaultCharset()); try { writer.write(data); @@ -71,6 +117,9 @@ public abstract class FileConfiguration extends MemoryConfiguration { * If the file does not exist, it will be created. If already exists, it * will be overwritten. If it cannot be overwritten or created, an * exception will be thrown. + *

+ * This method will save using the system default encoding, or possibly + * using UTF8. * * @param file File to save to. * @throws IOException Thrown when the given file cannot be written to for @@ -99,6 +148,10 @@ public abstract class FileConfiguration extends MemoryConfiguration { *

* If the file cannot be loaded for any reason, an exception will be * thrown. + *

+ * This will attempt to use the {@link Charset#defaultCharset()} for + * files, unless {@link #UTF8_OVERRIDE} but not {@link #UTF_BIG} is + * specified. * * @param file File to load from. * @throws FileNotFoundException Thrown when the given file cannot be @@ -111,7 +164,9 @@ public abstract class FileConfiguration extends MemoryConfiguration { public void load(File file) throws FileNotFoundException, IOException, InvalidConfigurationException { Validate.notNull(file, "File cannot be null"); - load(new FileInputStream(file)); + final FileInputStream stream = new FileInputStream(file); + + load(new InputStreamReader(stream, UTF8_OVERRIDE && !UTF_BIG ? Charsets.UTF_8 : Charset.defaultCharset())); } /** @@ -120,20 +175,42 @@ public abstract class FileConfiguration extends MemoryConfiguration { * All the values contained within this configuration will be removed, * leaving only settings and defaults, and the new values will be loaded * from the given stream. + *

+ * This will attempt to use the {@link Charset#defaultCharset()}, unless + * {@link #UTF8_OVERRIDE} or {@link #UTF_BIG} is specified. * * @param stream Stream to load from * @throws IOException Thrown when the given file cannot be read. * @throws InvalidConfigurationException Thrown when the given file is not * a valid Configuration. * @throws IllegalArgumentException Thrown when stream is null. + * @deprecated This does not consider encoding + * @see #load(Reader) */ + @Deprecated public void load(InputStream stream) throws IOException, InvalidConfigurationException { Validate.notNull(stream, "Stream cannot be null"); - InputStreamReader reader = new InputStreamReader(stream); - StringBuilder builder = new StringBuilder(); - BufferedReader input = new BufferedReader(reader); + load(new InputStreamReader(stream, UTF8_OVERRIDE ? Charsets.UTF_8 : Charset.defaultCharset())); + } + /** + * Loads this {@link FileConfiguration} from the specified reader. + *

+ * All the values contained within this configuration will be removed, + * leaving only settings and defaults, and the new values will be loaded + * from the given stream. + * + * @param reader the reader to load from + * @throws IOException thrown when underlying reader throws an IOException + * @throws InvalidConfigurationException thrown when the reader does not + * represent a valid Configuration + * @throws IllegalArgumentException thrown when reader is null + */ + public void load(Reader reader) throws IOException, InvalidConfigurationException { + BufferedReader input = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader); + + StringBuilder builder = new StringBuilder(); try { String line; diff --git a/paper-api/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java b/paper-api/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java index 18be0dc624..ea4c2b3540 100644 --- a/paper-api/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java +++ b/paper-api/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.util.Map; import java.util.logging.Level; @@ -32,6 +33,7 @@ public class YamlConfiguration extends FileConfiguration { public String saveToString() { yamlOptions.setIndent(options().indent()); yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + yamlOptions.setAllowUnicode(SYSTEM_UTF); yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); String header = buildHeader(); @@ -162,6 +164,8 @@ public class YamlConfiguration extends FileConfiguration { * Any errors loading the Configuration will be logged and then ignored. * If the specified input is not a valid config, a blank config will be * returned. + *

+ * The encoding used may follow the system dependent default. * * @param file Input file * @return Resulting configuration @@ -194,7 +198,11 @@ public class YamlConfiguration extends FileConfiguration { * @param stream Input stream * @return Resulting configuration * @throws IllegalArgumentException Thrown if stream is null + * @deprecated does not properly consider encoding + * @see #load(InputStream) + * @see #loadConfiguration(Reader) */ + @Deprecated public static YamlConfiguration loadConfiguration(InputStream stream) { Validate.notNull(stream, "Stream cannot be null"); @@ -210,4 +218,32 @@ public class YamlConfiguration extends FileConfiguration { return config; } + + + /** + * Creates a new {@link YamlConfiguration}, loading from the given reader. + *

+ * Any errors loading the Configuration will be logged and then ignored. + * If the specified input is not a valid config, a blank config will be + * returned. + * + * @param reader input + * @return resulting configuration + * @throws IllegalArgumentException Thrown if stream is null + */ + public static YamlConfiguration loadConfiguration(Reader reader) { + Validate.notNull(reader, "Stream cannot be null"); + + YamlConfiguration config = new YamlConfiguration(); + + try { + config.load(reader); + } catch (IOException ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load configuration from stream", ex); + } catch (InvalidConfigurationException ex) { + Bukkit.getLogger().log(Level.SEVERE, "Cannot load configuration from stream", ex); + } + + return config; + } } diff --git a/paper-api/src/main/java/org/bukkit/plugin/PluginAwareness.java b/paper-api/src/main/java/org/bukkit/plugin/PluginAwareness.java new file mode 100644 index 0000000000..ddb47b7e6a --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/plugin/PluginAwareness.java @@ -0,0 +1,29 @@ +package org.bukkit.plugin; + +import java.util.Set; + +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Represents a concept that a plugin is aware of. + *

+ * The internal representation may be singleton, or be a parameterized + * instance, but must be immutable. + */ +public interface PluginAwareness { + /** + * Each entry here represents a particular plugin's awareness. These can + * be checked by using {@link PluginDescriptionFile#getAwareness()}.{@link + * Set#contains(Object) contains(flag)}. + */ + public enum Flags implements PluginAwareness { + /** + * This specifies that all (text) resources stored in a plugin's jar + * use UTF-8 encoding. + * + * @see JavaPlugin#getTextResource(String) + */ + UTF8, + ; + } +} diff --git a/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java b/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java index cfd4b71b08..0fd966c6e4 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java +++ b/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java @@ -4,8 +4,10 @@ import java.io.InputStream; import java.io.Reader; import java.io.Writer; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.bukkit.command.CommandExecutor; import org.bukkit.command.PluginCommand; @@ -15,10 +17,14 @@ import org.bukkit.permissions.Permissible; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.AbstractConstruct; import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.Tag; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; /** * This type is the runtime-container for the information in the plugin.yml. @@ -111,6 +117,10 @@ import com.google.common.collect.ImmutableMap; * The default {@link Permission#getDefault() default} permission * state for defined {@link #getPermissions() permissions} the plugin * will register + * + * awareness + * {@link #getAwareness()} + * The concepts that the plugin acknowledges * * *

@@ -165,7 +175,39 @@ import com.google.common.collect.ImmutableMap; * */ public final class PluginDescriptionFile { - private static final Yaml yaml = new Yaml(new SafeConstructor()); + private static final ThreadLocal YAML = new ThreadLocal() { + @Override + protected Yaml initialValue() { + return new Yaml(new SafeConstructor() { + { + yamlConstructors.put(null, new AbstractConstruct() { + @Override + public Object construct(final Node node) { + if (!node.getTag().startsWith("!@")) { + // Unknown tag - will fail + return SafeConstructor.undefinedConstructor.construct(node); + } + // Unknown awareness - provide a graceful substitution + return new PluginAwareness() { + @Override + public String toString() { + return node.toString(); + } + }; + } + }); + for (final PluginAwareness.Flags flag : PluginAwareness.Flags.values()) { + yamlConstructors.put(new Tag("!@" + flag.name()), new AbstractConstruct() { + @Override + public PluginAwareness.Flags construct(final Node node) { + return flag; + } + }); + } + } + }); + } + }; String rawName = null; private String name = null; private String main = null; @@ -184,9 +226,10 @@ public final class PluginDescriptionFile { private List permissions = null; private Map lazyPermissions = null; private PermissionDefault defaultPerm = PermissionDefault.OP; + private Set awareness = ImmutableSet.of(); public PluginDescriptionFile(final InputStream stream) throws InvalidDescriptionException { - loadMap(asMap(yaml.load(stream))); + loadMap(asMap(YAML.get().load(stream))); } /** @@ -197,7 +240,7 @@ public final class PluginDescriptionFile { * invalid */ public PluginDescriptionFile(final Reader reader) throws InvalidDescriptionException { - loadMap(asMap(yaml.load(reader))); + loadMap(asMap(YAML.get().load(reader))); } /** @@ -767,6 +810,45 @@ public final class PluginDescriptionFile { return defaultPerm; } + /** + * Gives a set of every {@link PluginAwareness} for a plugin. An awareness + * dictates something that a plugin developer acknowledges when the plugin + * is compiled. Some implementions may define extra awarenesses that are + * not included in the API. Any unrecognized + * awareness (one unsupported or in a future version) will cause a dummy + * object to be created instead of failing. + *

+ *

+ *

+ * In the plugin.yml, this entry is named awareness. + *

+ * Example:

awareness:
+     *- !@UTF8
+ *

+ * Note: Although unknown versions of some future awareness are + * gracefully substituted, previous versions of Bukkit (ones prior to the + * first implementation of awareness) will fail to load a plugin that + * defines any awareness. + * + * @return a set containing every awareness for the plugin + */ + public Set getAwareness() { + return awareness; + } + /** * Returns the name of a plugin, including the version. This method is * provided for convenience; it uses the {@link #getName()} and {@link @@ -796,7 +878,7 @@ public final class PluginDescriptionFile { * @param writer Writer to output this file to */ public void save(Writer writer) { - yaml.dump(saveMap(), writer); + YAML.get().dump(saveMap(), writer); } private void loadMap(Map map) throws InvalidDescriptionException { @@ -926,6 +1008,18 @@ public final class PluginDescriptionFile { } } + if (map.get("awareness") instanceof Iterable) { + Set awareness = new HashSet(); + try { + for (Object o : (Iterable) map.get("awareness")) { + awareness.add((PluginAwareness) o); + } + } catch (ClassCastException ex) { + throw new InvalidDescriptionException(ex, "awareness has wrong type"); + } + this.awareness = ImmutableSet.copyOf(awareness); + } + try { lazyPermissions = (Map) map.get("permissions"); } catch (ClassCastException ex) { diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java index a0b609fab0..19893f3151 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java +++ b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPlugin.java @@ -4,9 +4,12 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.Reader; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; @@ -18,10 +21,12 @@ import org.bukkit.Warning.WarningState; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.PluginCommand; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.generator.ChunkGenerator; import org.bukkit.plugin.AuthorNagException; +import org.bukkit.plugin.PluginAwareness; import org.bukkit.plugin.PluginBase; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginLoader; @@ -33,6 +38,8 @@ import com.avaje.ebean.config.DataSourceConfig; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebeaninternal.api.SpiEbeanServer; import com.avaje.ebeaninternal.server.ddl.DdlGenerator; +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; /** * Represents a Java plugin @@ -89,6 +96,7 @@ public abstract class JavaPlugin extends PluginBase { * * @return The folder. */ + @Override public final File getDataFolder() { return dataFolder; } @@ -98,6 +106,7 @@ public abstract class JavaPlugin extends PluginBase { * * @return PluginLoader that controls this plugin */ + @Override public final PluginLoader getPluginLoader() { return loader; } @@ -107,6 +116,7 @@ public abstract class JavaPlugin extends PluginBase { * * @return Server running this plugin */ + @Override public final Server getServer() { return server; } @@ -117,6 +127,7 @@ public abstract class JavaPlugin extends PluginBase { * * @return true if this plugin is enabled, otherwise false */ + @Override public final boolean isEnabled() { return isEnabled; } @@ -135,10 +146,12 @@ public abstract class JavaPlugin extends PluginBase { * * @return Contents of the plugin.yaml file */ + @Override public final PluginDescriptionFile getDescription() { return description; } + @Override public FileConfiguration getConfig() { if (newConfig == null) { reloadConfig(); @@ -146,17 +159,67 @@ public abstract class JavaPlugin extends PluginBase { return newConfig; } + /** + * Provides a reader for a text file located inside the jar. The behavior + * of this method adheres to {@link PluginAwareness.Flags#UTF8}, or if not + * defined, uses UTF8 if {@link FileConfiguration#UTF8_OVERRIDE} is + * specified, or system default otherwise. + * + * @param file the filename of the resource to load + * @return null if {@link #getResource(String)} returns null + * @throws IllegalArgumentException if file is null + * @see ClassLoader#getResourceAsStream(String) + */ + @SuppressWarnings("deprecation") + protected final Reader getTextResource(String file) { + final InputStream in = getResource(file); + + return in == null ? null : new InputStreamReader(in, isStrictlyUTF8() || FileConfiguration.UTF8_OVERRIDE ? Charsets.UTF_8 : Charset.defaultCharset()); + } + + @SuppressWarnings("deprecation") + @Override public void reloadConfig() { newConfig = YamlConfiguration.loadConfiguration(configFile); - InputStream defConfigStream = getResource("config.yml"); - if (defConfigStream != null) { - YamlConfiguration defConfig = YamlConfiguration.loadConfiguration(defConfigStream); - - newConfig.setDefaults(defConfig); + final InputStream defConfigStream = getResource("config.yml"); + if (defConfigStream == null) { + return; } + + final YamlConfiguration defConfig; + if (isStrictlyUTF8() || FileConfiguration.UTF8_OVERRIDE) { + defConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, Charsets.UTF_8)); + } else { + final byte[] contents; + defConfig = new YamlConfiguration(); + try { + contents = ByteStreams.toByteArray(defConfigStream); + } catch (final IOException e) { + getLogger().log(Level.SEVERE, "Unexpected failure reading config.yml", e); + return; + } + + final String text = new String(contents, Charset.defaultCharset()); + if (!text.equals(new String(contents, Charsets.UTF_8))) { + getLogger().warning("Default system encoding may have misread config.yml from plugin jar"); + } + + try { + defConfig.loadFromString(text); + } catch (final InvalidConfigurationException e) { + getLogger().log(Level.SEVERE, "Cannot load configuration from jar", e); + } + } + + newConfig.setDefaults(defConfig); } + private boolean isStrictlyUTF8() { + return getDescription().getAwareness().contains(PluginAwareness.Flags.UTF8); + } + + @Override public void saveConfig() { try { getConfig().save(configFile); @@ -165,12 +228,14 @@ public abstract class JavaPlugin extends PluginBase { } } + @Override public void saveDefaultConfig() { if (!configFile.exists()) { saveResource("config.yml", false); } } + @Override public void saveResource(String resourcePath, boolean replace) { if (resourcePath == null || resourcePath.equals("")) { throw new IllegalArgumentException("ResourcePath cannot be null or empty"); @@ -208,6 +273,7 @@ public abstract class JavaPlugin extends PluginBase { } } + @Override public InputStream getResource(String filename) { if (filename == null) { throw new IllegalArgumentException("Filename cannot be null"); @@ -328,6 +394,7 @@ public abstract class JavaPlugin extends PluginBase { /** * {@inheritDoc} */ + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { return false; } @@ -335,6 +402,7 @@ public abstract class JavaPlugin extends PluginBase { /** * {@inheritDoc} */ + @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { return null; } @@ -362,24 +430,31 @@ public abstract class JavaPlugin extends PluginBase { } } + @Override public void onLoad() {} + @Override public void onDisable() {} + @Override public void onEnable() {} + @Override public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { return null; } + @Override public final boolean isNaggable() { return naggable; } + @Override public final void setNaggable(boolean canNag) { this.naggable = canNag; } + @Override public EbeanServer getDatabase() { return ebean; } @@ -398,6 +473,7 @@ public abstract class JavaPlugin extends PluginBase { gen.runScript(true, gen.generateDropDdl()); } + @Override public final Logger getLogger() { return logger; }