Change YamlConfiguration encoding styles.

On JVMs with UTF-8 default encoding, this commit has no change in behavior.

On JVMs with ascii default encoding (like some minimal linux installa-
tions), this commit now uses UTF-8 for YamlConfiguration operations.
Because all ascii is valid UTF-8, there is no feature degradation or data
loss during the transition.

On JVMs with any non-unicode but ascii-compliant encoding, this commit now
forces YamlConfiguration to escape special characters when writing to
files, effectively rendering the encoding to be plain ascii. Any affected
file will now be able to migrate to UTF-8 in the future without data-loss
or explicit conversion. When reading files, YamlConfiguration will use the
system default encoding to handle any incoming non-utf8 data, with the
expectation that any newly written file is still compliant with the
system's default encoding.

On JVMs with any non-unicode, but ascii-incompliant encoding (this may be
the case for some Eastern character sets on Windows systems), this change
is breaking, but is justified in claim that these systems would otherwise
be unable to read YamlConfiguration for implementation dependent settings
or from plugins themselves. For these systems, all uses of the encoding
will be forced to use UTF-8 in all cases, and is effectively treated as if
it was configured to be UTF-8 by default.

On JVMs with unicode encoding of UTF-16 or UTF-32, the ability to load any
configurations from almost any source prior to this change would have been
unfeasible, if not impossible. As of this change, however, these systems
now behave as expected when writing or reading files. However, when
reading from any plugin jar, UTF-8 will be used, matching a super-majority
of plugin developer base and requirements for the plugin.yml.

Plugin developers may now mark their plugin as UTF-8 compliant, as
documented in the PluginDescriptionFile class. This change will cause the
appropriate APIs in JavaPlugin to ignore any system default encoding,
instead using a Reader with the UTF-8 encoding, effectively rendering the
jar system independent. This does not affect the aformentioned JVM
settings for reading and writing files.

To coincide with these changes, YamlConfiguration methods that utilize a
stream are now deprecated to encourage use of a more strict denotation.
File methods carry system-specific behaviors to prevent unncessary data
loss during the transitional phase, while Reader methods are now provided
that have a very well-defined encoder behavior. For the transition from
InputStream methods to Reader methods, an API has been added to JavaPlugin
to provide a Reader that matches the previous behavior as well as
compliance to the UTF-8 flag in the PluginDescriptionFile.

Addresses BUKKIT-314, BUKKIT-1466, BUKKIT-3377

By: Wesley Wolfe <wesley.d.wolfe+git@gmail.com>
This commit is contained in:
Bukkit/Spigot
2014-03-25 00:05:21 -05:00
parent 14d4cd125c
commit 300e61658e
5 changed files with 327 additions and 15 deletions

View File

@@ -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)));
}
/**
@@ -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.
* <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
@@ -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) {