diff options
5 files changed, 326 insertions, 14 deletions
diff --git a/src/main/java/org/bukkit/configuration/file/FileConfiguration.java b/src/main/java/org/bukkit/configuration/file/FileConfiguration.java index 3f9992e7..9d6d1c61 100644 --- a/src/main/java/org/bukkit/configuration/file/FileConfiguration.java +++ b/src/main/java/org/bukkit/configuration/file/FileConfiguration.java @@ -1,19 +1,27 @@ 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 @@ -21,6 +29,41 @@ import org.bukkit.configuration.MemoryConfiguration; */ 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. */ public FileConfiguration() { @@ -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. + * <p> + * 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. + * <p> + * 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 { * <p> * If the file cannot be loaded for any reason, an exception will be * thrown. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java b/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java index 18be0dc6..ea4c2b35 100644 --- a/src/main/java/org/bukkit/configuration/file/YamlConfiguration.java +++ b/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. + * <p> + * 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. + * <p> + * 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/src/main/java/org/bukkit/plugin/PluginAwareness.java b/src/main/java/org/bukkit/plugin/PluginAwareness.java new file mode 100644 index 00000000..ddb47b7e --- /dev/null +++ b/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. + * <p> + * 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/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java b/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java index cfd4b71b..0fd966c6 100644 --- a/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java +++ b/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; * <td>The default {@link Permission#getDefault() default} permission * state for defined {@link #getPermissions() permissions} the plugin * will register</td> + * </tr><tr> + * <td><code>awareness</code></td> + * <td>{@link #getAwareness()}</td> + * <td>The concepts that the plugin acknowledges</td> * </tr> * </table> * <p> @@ -165,7 +175,39 @@ import com.google.common.collect.ImmutableMap; *</pre></blockquote> */ public final class PluginDescriptionFile { - private static final Yaml yaml = new Yaml(new SafeConstructor()); + private static final ThreadLocal<Yaml> YAML = new ThreadLocal<Yaml>() { + @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<Permission> permissions = null; private Map<?, ?> lazyPermissions = null; private PermissionDefault defaultPerm = PermissionDefault.OP; + private Set<PluginAwareness> 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))); } /** @@ -768,6 +811,45 @@ public final class PluginDescriptionFile { } /** + * 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. + * <p> + * <ul> + * <li>Currently only supports the enumerated values in {@link + * PluginAwareness.Flags}. + * <li>Each awareness starts the identifier with bang-at + * (<code>!@</code>). + * <li>Unrecognized (future / unimplemented) entries are quietly replaced + * by a generic object that implements PluginAwareness. + * <li>A type of awareness must be defined by the runtime and acknowledged + * by the API, effectively discluding any derived type from any + * plugin's classpath. + * <li><code>awareness</code> must be in <a + * href="http://en.wikipedia.org/wiki/YAML#Lists">YAML list + * format</a>. + * </ul> + * <p> + * In the plugin.yml, this entry is named <code>awareness</code>. + * <p> + * Example:<blockquote><pre>awareness: + *- !@UTF8</pre></blockquote> + * <p> + * <b>Note:</b> 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<PluginAwareness> 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 * #getVersion()} entries. @@ -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<PluginAwareness> awareness = new HashSet<PluginAwareness>(); + 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/src/main/java/org/bukkit/plugin/java/JavaPlugin.java b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java index a0b609fa..19893f31 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java +++ b/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); + 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"); + } - newConfig.setDefaults(defConfig); + 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<String> 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; } |