WorldGuard/src/main/java/com/sk89q/worldguard/region/stores/YamlStore.java

336 lines
12 KiB
Java

// $Id$
/*
* WorldGuard
* Copyright (C) 2010 sk89q <http://www.sk89q.com>
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.region.stores;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;
import org.apache.commons.lang.Validate;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import com.sk89q.rebar.config.ConfigurationException;
import com.sk89q.rebar.config.ConfigurationNode;
import com.sk89q.rebar.config.YamlConfiguration;
import com.sk89q.rebar.config.YamlConfigurationFile;
import com.sk89q.rebar.config.YamlStyle;
import com.sk89q.rebar.config.types.BlockVector2dLoaderBuilder;
import com.sk89q.rebar.config.types.ListBuilder;
import com.sk89q.rebar.config.types.VectorLoaderBuilder;
import com.sk89q.rebar.util.LoggerUtils;
import com.sk89q.worldedit.BlockVector2D;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldguard.region.Region;
import com.sk89q.worldguard.region.indices.RegionIndex;
import com.sk89q.worldguard.region.indices.RegionIndexFactory;
import com.sk89q.worldguard.region.shapes.Cuboid;
import com.sk89q.worldguard.region.shapes.Everywhere;
import com.sk89q.worldguard.region.shapes.ExtrudedPolygon;
import com.sk89q.worldguard.region.shapes.IndexableShape;
/**
* A YAML-based region store that uses {@link YamlConfiguration} in order to store
* region data.
* <p>
* The main advantage of this format is that output files can easily be edited by
* hand, but the disadvantage is that it's not fast as a format that writes more
* native data types. In addition, while YAML is not nearly verbose as XML, being a
* text-based format making heavy use of indentation, the size of data files written
* by this format can be quite large.
* <p>
* To mitigate some of the impacts of this format, this store uses only an indentation
* size of two spaces to reduce disk space usage.
*/
public class YamlStore implements RegionStore {
private static final Logger defaultLogger = LoggerUtils.getLogger(YamlStore.class);
private static final YamlStyle style = new YamlStyle(FlowStyle.FLOW, 2);
private static final VectorLoaderBuilder vectorLB = new VectorLoaderBuilder();
private static final BlockVector2dLoaderBuilder blockVec2dLB = new BlockVector2dLoaderBuilder();
private Logger logger = defaultLogger;
private final File file;
/**
* Create a new YAML-based store that uses the provided file for storing data.
* <p>
* The file does not yet have to exist, but it does need to be readable and
* writable in order for regions to be loaded or saved.
*
* @param file file to store data in
*/
public YamlStore(File file) {
this.file = file;
}
/**
* Get the logger assigned to this store.
* <p>
* Messages will be relayed through the logger set on this store if possible.
*
* @return logger, or null
*/
public Logger getLogger() {
return logger;
}
/**
* Set the logger assigned to this store.
* <p>
* Messages will be relayed through the logger set on this store if possible.
*
* @param logger or null
*/
public void setLogger(Logger logger) {
this.logger = logger;
}
@Override
public RegionIndex load(RegionIndexFactory factory) throws IOException {
YamlConfiguration config = new YamlConfigurationFile(file);
try {
config.load(); // Load data (may raise an exception)
} catch (ConfigurationException e) {
throw new IOException("Fatal syntax or other error in YAML-formatted data", e);
}
// Create an index
RegionIndex index = factory.newIndex();
// Store a list of parent relationships that we have to set later on
Map<Region, String> parentSets = new LinkedHashMap<Region, String>();
for (Entry<String, ConfigurationNode> entry : config.getNodes("regions").entrySet()) {
ConfigurationNode node = entry.getValue();
try {
IndexableShape shape;
String id = node.getString("id");
String type = node.getString("type");
Validate.notNull(id, "Missing an 'id' parameter for: " + node);
Validate.notNull(type, "Missing a 'type' parameter for: " + node);
// Axis-aligned cuboid type
if (type.equals("cuboid")) {
Vector pt1 = config.getOf("min", vectorLB);
Vector pt2 = config.getOf("max", vectorLB);
Validate.notNull(pt1, "Missing a 'min' parameter for: " + node);
Validate.notNull(pt2, "Missing a 'max' parameter for: " + node);
shape = new Cuboid(pt1, pt2);
// Extruded polygon type
} else if (type.equals("poly2d")) {
Integer minY = node.getInt("min-y");
Integer maxY = node.getInt("max-y");
Validate.notNull(minY, "Missing a 'min-y' parameter for: " + node);
Validate.notNull(maxY, "Missing a 'max-y' parameter for: " + node);
List<BlockVector2D> points = node.listOf("points", blockVec2dLB);
// Note: Invalid points are discarded!
shape = new ExtrudedPolygon(points, minY, maxY);
// "Everywhere" type
} else if (type.equals("global")) {
shape = new Everywhere();
// ???
} else {
throw new IllegalArgumentException("Don't know what type of shape '" +
type + "' is! In: " + node);
}
Region region = new Region(id, shape);
region.setPriority(node.getInt("priority", 0));
loadFlags(region, node.getNode("flags")); // Legacy flags support
loadAttributes(region, node.getNode("attr"));
// Remember the parent so that it can be linked lateron
String parentId = node.getString("parent");
if (parentId != null) {
parentSets.put(region, parentId);
}
} catch (IllegalArgumentException e) {
logger.warning(e.getMessage());
}
}
// Re-link parents
for (Map.Entry<Region, String> entry : parentSets.entrySet()) {
Region parent = index.get(entry.getValue());
if (parent != null) {
try {
entry.getKey().setParent(parent);
} catch (IllegalArgumentException e) {
logger.warning("Circular inheritance detect with '"
+ entry.getValue() + "' detected as a parent");
}
} else {
logger.warning("Unknown region parent: " + entry.getValue());
}
}
index.reindex(); // Important!
return index;
}
@Override
public void save(Collection<Region> regions) throws IOException {
YamlConfiguration config = new YamlConfigurationFile(file, style);
for (Region region : regions) {
ConfigurationNode node = config.setNode("regions").setNode(region.getId());
IndexableShape shape = region.getShape();
if (shape instanceof Cuboid) {
Cuboid cuboid = (Cuboid) shape;
node.set("type", "cuboid");
node.set("min", cuboid.getAABBMin(), blockVec2dLB);
node.set("max", cuboid.getAABBMax(), blockVec2dLB);
} else if (shape instanceof ExtrudedPolygon) {
ExtrudedPolygon poly = (ExtrudedPolygon) shape;
node.set("type", "poly2d");
node.set("min-y", poly.getAABBMin().getBlockY());
node.set("max-y", poly.getAABBMax().getBlockY());
node.set("points", poly.getProjectedVerts(),
new ListBuilder<BlockVector2D>(blockVec2dLB));
} else if (shape instanceof Everywhere) {
node.set("type", "global");
} else {
// This means that it's not supported!
node.set("type", region.getClass().getCanonicalName());
}
node.set("priority", region.getPriority());
node.set("flags", buildFlags(region));
Region parent = region.getParent();
if (parent != null) {
node.set("parent", parent.getId());
}
}
config.setHeader("#\r\n" +
"# WARNING: THIS FILE IS AUTOMATICALLY GENERATED.\r\n" +
"# A minor error in this file WILL DESTROY ALL YOUR DATA.\r\n" +
"#\r\n" +
"# REMEMBER TO KEEP PERIODICAL BACKUPS.\r\n" +
"#");
config.save();
}
/**
* Read flag data from the given node and apply it to the region.
*
* @param region region to apply flags to
* @param node node, or null
*/
private void loadFlags(Region region, ConfigurationNode node) {
if (node == null) {
return;
}
for (Flag<?> flag : DefaultFlag.getFlags()) {
Flag<?> groupFlag = flag.getRegionGroupFlag();
Object rawValue = node.get(flag.getName());
if (rawValue != null) {
setFlagFromRaw(region, flag, rawValue);
}
// Also get the group flag
if (groupFlag != null) {
rawValue = node.get(groupFlag.getName());
if (rawValue != null) {
setFlagFromRaw(region, groupFlag, rawValue);
}
}
}
}
private void loadAttributes(Region region, ConfigurationNode node) {
}
/**
* Try to set a flag on a region from its raw value. The flag will be properly
* unmarshalled, and if that fails, then the flag won't be set.
*
* @param region region to affect
* @param flag the flag to set
* @param rawValue the raw value before unmarshalling
*/
private void setFlagFromRaw(Region region, Flag<?> flag, Object rawValue) {
Object value = flag.unmarshal(rawValue);
if (value == null) {
logger.warning("Failed to parse flag '" + flag.getName()
+ "' with value '" + rawValue.toString() + "'");
} else {
region.setFlagUnsafe(flag, flag.unmarshal(rawValue));
}
}
/**
* Build a map of flag data to store to disk.
*
* @param region the region to read the flags from
* @return map of flag data
*/
private Map<String, Object> buildFlags(Region region) {
Map<String, Object> data = new HashMap<String, Object>();
for (Map.Entry<Flag<?>, Object> entry : region.getFlags().entrySet()) {
storeFlagFromValue(data, entry.getKey(), entry.getValue());
}
return data;
}
/**
* Marshal a flag's value into something YAML-compatible.
* <p>
* if the given value is null, the flag is ignored.
*
* @param data map to put the data into
* @param flag the flag
* @param val the value, or null
*/
@SuppressWarnings("unchecked")
private <V> void storeFlagFromValue(Map<String, Object> data, Flag<V> flag, Object val) {
if (val == null) {
return;
} else {
data.put(flag.getName(), flag.marshal((V) val));
}
}
@Override
public void close() throws IOException {
// Do nothing
}
}