package tc.oc.pgm.utils; import java.io.IOException; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.base.Splitter; import com.google.common.collect.BoundType; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.common.collect.Sets; import com.google.gson.JsonParseException; import gnu.trove.list.TIntList; import gnu.trove.list.array.TIntArrayList; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TranslatableComponent; import net.md_5.bungee.chat.ComponentSerializer; import org.apache.commons.codec.binary.Base64; import org.bukkit.Bukkit; import org.bukkit.DyeColor; import org.bukkit.GameMode; import org.bukkit.Material; import org.bukkit.Skin; import org.bukkit.attribute.AttributeModifier; import org.bukkit.attribute.ItemAttributeModifier; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Entity; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.material.MaterialData; import org.bukkit.potion.PotionBrew; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.registry.Key; import org.bukkit.scoreboard.Team; import org.bukkit.util.BlockVector; import org.bukkit.util.ImVector; import org.bukkit.util.Vector; import org.jdom2.Attribute; import org.jdom2.Element; import tc.oc.api.docs.SemanticVersion; import tc.oc.commons.bukkit.inventory.Slot; import tc.oc.commons.bukkit.localization.Translations; import tc.oc.commons.bukkit.util.BukkitUtils; import tc.oc.commons.bukkit.util.NMSHacks; import tc.oc.commons.core.chat.Component; import tc.oc.commons.core.chat.Components; import tc.oc.commons.core.util.ArrayUtils; import tc.oc.commons.core.util.NumberFactory; import tc.oc.commons.core.util.Pair; import tc.oc.commons.core.util.TimeUtils; import tc.oc.pgm.xml.BoundedElement; import tc.oc.pgm.xml.ElementFlattener; import tc.oc.pgm.xml.InvalidXMLException; import tc.oc.pgm.xml.Node; import tc.oc.pgm.xml.parser.BooleanParser; import tc.oc.pgm.xml.parser.NumberParser; import tc.oc.pgm.xml.parser.PrimitiveParser; import tc.oc.pgm.xml.parser.StringParser; import tc.oc.pgm.xml.parser.TeamRelationParser; import tc.oc.pgm.xml.parser.VectorParser; import tc.oc.pgm.xml.property.DurationProperty; import tc.oc.pgm.xml.property.NumberProperty; import tc.oc.pgm.xml.property.PropertyBuilder; import tc.oc.pgm.xml.property.PropertyBuilderFactory; public class XMLUtils { @Inject private static PropertyBuilderFactory<Duration, DurationProperty> durationFactory; @Inject private static PrimitiveParser<org.bukkit.attribute.Attribute> attributeParser; /** * @param parentTagNames Names of elements deeper than the root that can be traversed * @param childTagNames Names of elements that can be included in the result * @param minChildDepth Minimum depth of elements that can be in the result, relative to the children of root */ public static List<Element> flattenElements(Element root, Set<String> parentTagNames, @Nullable Set<String> childTagNames, int minChildDepth) { return new ElementFlattener(parentTagNames, childTagNames, minChildDepth) .flattenChildren(root) .collect(Collectors.toList()); } public static List<Element> flattenElements(Element root, Set<String> parentTagNames, @Nullable Set<String> childTagNames) { return flattenElements(root, parentTagNames, childTagNames, 1); } public static List<Element> flattenElements(Element root, Set<String> parentTagNames) { return flattenElements(root, parentTagNames, null); } public static List<Element> flattenElements(Element root, String parentTagName, @Nullable String childTagName, int minChildDepth) { return flattenElements(root, ImmutableSet.of(parentTagName), childTagName == null ? null : ImmutableSet.of(childTagName), minChildDepth); } public static List<Element> flattenElements(Element root, String parentTagName, @Nullable String childTagName) { return flattenElements(root, parentTagName, childTagName, 1); } public static List<Element> flattenElements(Element root, String parentTagName) { return flattenElements(root, parentTagName, null); } public static Iterable<Element> getChildren(Element parent, Collection<String> names) { return Iterables.filter(parent.getChildren(), el -> names.contains(el.getName())); } public static Iterable<Element> getChildren(Element parent, String...names) { return getChildren(parent, Arrays.asList(names)); } public static @Nullable Attribute getAttribute(Element parent, Collection<String> names) { for(String name : names) { final Attribute attr = parent.getAttribute(name); if(attr != null) return attr; } return null; } public static @Nullable Attribute getAttribute(Element parent, String...names) { return getAttribute(parent, Arrays.asList(names)); } public static Iterable<Attribute> getAttributes(Element parent, Collection<String> names) { return Iterables.filter(parent.getAttributes(), attr -> names.contains(attr.getName())); } public static Iterable<Attribute> getAttributes(Element parent, String...names) { return getAttributes(parent, Arrays.asList(names)); } public static @Nullable Element getUniqueChild(Element parent, String name, String...aliases) throws InvalidXMLException { return getUniqueChild(parent, name, ImmutableSet.copyOf(aliases)); } public static @Nullable Element getUniqueChild(Element parent, String name, Set<String> aliases) throws InvalidXMLException { List<Element> children = new ArrayList<>(); for(String alias : Sets.union(ImmutableSet.of(name), aliases)) { children.addAll(parent.getChildren(alias)); } if(children.size() > 1) { throw new InvalidXMLException("multiple '" + name + "' tags not allowed", parent); } return children.isEmpty() ? null : children.get(0); } public static Element getRequiredUniqueChild(Element parent, String name, String...aliases) throws InvalidXMLException { aliases = ArrayUtils.append(aliases, name); List<Element> children = new ArrayList<>(); for(String alias : aliases) { children.addAll(parent.getChildren(alias)); } if(children.size() > 1) { throw new InvalidXMLException("multiple '" + name + "' tags not allowed", parent); } else if (children.isEmpty()) { throw new InvalidXMLException("child tag '" + name + "' is required", parent); } return children.get(0); } public static Element getRequiredUniqueChild(Element parent) throws InvalidXMLException { if(parent.getChildren().size() > 1) { throw new InvalidXMLException("multiple elements not allowed", parent); } else if (parent.getChildren().isEmpty()) { throw new InvalidXMLException("child element is required", parent); } return parent.getChildren().get(0); } public static Attribute getRequiredAttribute(Element el, String name, String...aliases) throws InvalidXMLException { aliases = ArrayUtils.append(aliases, name); Attribute attr = null; for(String alias : aliases) { Attribute a = el.getAttribute(alias); if(a != null) { if(attr == null) { attr = a; } else { throw new InvalidXMLException("attributes '" + attr.getName() + "' and '" + alias + "' are aliases for the same thing, and cannot be combined", el); } } } if(attr == null) { throw new InvalidXMLException("attribute '" + name + "' is required", el); } return attr; } public static PropertyBuilder<Boolean, ?> parseBoolean(Element parent, String name) { return new PropertyBuilder<>(parent, name, BooleanParser.get()); } private static Boolean parseBoolean(Node node, String value) throws InvalidXMLException { return BooleanParser.get().parse(node, value); } public static Boolean parseBoolean(Node node) throws InvalidXMLException { return node == null ? null : parseBoolean(node, node.getValue()); } public static Boolean parseBoolean(@Nullable Node node, Boolean def) throws InvalidXMLException { return node == null ? def : parseBoolean(node); } public static Boolean parseBoolean(@Nullable Element el, Boolean def) throws InvalidXMLException { return el == null ? def : parseBoolean(new Node(el)); } public static Boolean parseBoolean(@Nullable Attribute attr, Boolean def) throws InvalidXMLException { return attr == null ? def : parseBoolean(new Node(attr)); } public static Optional<Boolean> parseOptionalBoolean(@Nullable Node node) throws InvalidXMLException { return node == null ? Optional.empty() : Optional.of(parseBoolean(node)); } public static <T extends Number & Comparable<T>> NumberProperty<T> parseNumber(Element parent, String name, Class<T> type) { return new NumberProperty<>(parent, name, type); } public static <T extends Number> T parseNumber(Node node, String text, Class<T> type, boolean infinity) throws InvalidXMLException { final T value = NumberParser.get(type).parse(node, text); if(!infinity && !NumberFactory.get(type).isFinite(value)) { throw new InvalidXMLException("Number must be finite", node); } return value; } public static <T extends Number> Optional<T> parseOptionalNumber(@Nullable Node node, Class<T> type) throws InvalidXMLException { return Optional.ofNullable(parseNumber(node, type, null)); } public static <T extends Number> T parseNumber(Node node, String text, Class<T> type, boolean infinity, T def) throws InvalidXMLException { return node == null ? def : parseNumber(node, text, type, infinity); } public static <T extends Number> T parseNumber(Node node, String text, Class<T> type, T def) throws InvalidXMLException { return parseNumber(node, text, type, false, def); } public static <T extends Number> T parseNumber(Node node, String text, Class<T> type) throws InvalidXMLException { return parseNumber(node, text, type, false); } public static <T extends Number> T parseNumber(Node node, Class<T> type, boolean infinity) throws InvalidXMLException { return parseNumber(node, node.getValue(), type, infinity); } public static <T extends Number> T parseNumber(Node node, Class<T> type) throws InvalidXMLException { return parseNumber(node, node.getValue(), type); } public static <T extends Number> T parseNumber(Attribute attr, Class<T> type) throws InvalidXMLException { return parseNumber(new Node(attr), type); } public static <T extends Number> T parseNumber(Element el, Class<T> type) throws InvalidXMLException { return parseNumber(new Node(el), type); } public static <T extends Number> T parseNumber(Node node, Class<T> type, boolean infinity, T def) throws InvalidXMLException { if(node == null) { return def; } else { return parseNumber(node, node.getValue(), type, infinity); } } public static <T extends Number> T parseNumber(Node node, Class<T> type, T def) throws InvalidXMLException { return parseNumber(node, type, false, def); } public static <T extends Number> T parseNumber(Element el, Class<T> type, T def) throws InvalidXMLException { if(el == null) { return def; } else { return parseNumber(el, type); } } public static <T extends Number> T parseNumber(Attribute attr, Class<T> type, T def) throws InvalidXMLException { if(attr == null) { return def; } else { return parseNumber(attr, type); } } // Note: all the range overloads allow infinities if they are within the range public static <T extends Number & Comparable<T>> T parseNumber(Element el, Class<T> type, Range<T> range) throws InvalidXMLException { return parseNumber(new Node(el), type, range); } public static <T extends Number & Comparable<T>> T parseNumber(Attribute attr, Class<T> type, Range<T> range) throws InvalidXMLException { return parseNumber(new Node(attr), type, range); } public static <T extends Number & Comparable<T>> T parseNumber(Node node, Class<T> type, Range<T> range) throws InvalidXMLException { return parseNumber(node, node.getValueNormalize(), type, range); } public static <T extends Number & Comparable<T>> T parseNumber(Node node, String text, Class<T> type, Range<T> range) throws InvalidXMLException { T value = parseNumber(node, text, type, true); if(!range.contains(value)) { throw new InvalidXMLException(value + " is not in the range " + range, node); } return value; } public static <T extends Number & Comparable<T>> T parseNumber(Node node, String text, Class<T> type, Range<T> range, T def) throws InvalidXMLException { return node == null ? def : parseNumber(node, text, type, range); } public static <T extends Number & Comparable<T>> T parseNumber(Node node, Class<T> type, Range<T> range, T def) throws InvalidXMLException { return node == null ? def : parseNumber(node, type, range); } private static final Pattern RANGE_RE = Pattern.compile("\\s*(\\(|\\[)\\s*([^,]+)\\s*,\\s*([^\\)\\]]+)\\s*(\\)|\\])\\s*"); /** * Parse a range in the standard mathematical format e.g. * * [0, 1) for a closed-open range from 0 to 1. * * Can also parse single numbers as a closed range e.g. * * 5 for a closed-closed range from 5 to 5. */ public static <T extends Number & Comparable<T>> Range<T> parseNumericRange(Node node, Class<T> type) throws InvalidXMLException { String value = node.getValue(); Matcher matcher = RANGE_RE.matcher(value); if(!matcher.matches()) { T number = parseNumber(node, value, type, (T) null); if(number != null) { return Range.singleton(number); } throw new InvalidXMLException("Invalid " + type.getSimpleName().toLowerCase() + " range '" + value + "'", node); } T lower = parseNumber(node, matcher.group(2), type, true); T upper = parseNumber(node, matcher.group(3), type, true); BoundType lowerType = null, upperType = null; if(!Double.isInfinite(lower.doubleValue())) { lowerType = "(".equals(matcher.group(1)) ? BoundType.OPEN : BoundType.CLOSED; } if(!Double.isInfinite(upper.doubleValue())) { upperType = ")".equals(matcher.group(4)) ? BoundType.OPEN : BoundType.CLOSED; } if(lower.compareTo(upper) == 1) { throw new InvalidXMLException("range lower bound (" + lower + ") cannot be greater than upper bound (" + upper + ")", node); } if(lowerType == null) { if(upperType == null) { return Range.all(); } else { return Range.upTo(upper, upperType); } } else { if(upperType == null) { return Range.downTo(lower, lowerType); } else { return Range.range(lower, lowerType, upper, upperType); } } } /** * Parse a numeric range from attributes on the given element specifying the bounds of the range, specifically: * * gt gte lt lte */ public static <T extends Number & Comparable<T>> Range<T> parseNumericRange(Element el, Class<T> type) throws InvalidXMLException { return parseNumericRange(el, type, Range.all()); } public static <T extends Number & Comparable<T>> Range<T> parseNumericRange(Element el, Class<T> type, Range<T> def) throws InvalidXMLException { Attribute lt = el.getAttribute("lt"); Attribute lte = getAttribute(el, "lte", "max"); Attribute gt = el.getAttribute("gt"); Attribute gte = getAttribute(el, "gte", "min"); if(lt != null && lte != null) throw new InvalidXMLException("Conflicting upper bound for numeric range", el); if(gt != null && gte != null) throw new InvalidXMLException("Conflicting lower bound for numeric range", el); BoundType lowerBoundType, upperBoundType; T lowerBound, upperBound; if(gt != null) { lowerBound = parseNumber(gt, type, (T) null); lowerBoundType = BoundType.OPEN; } else { lowerBound = parseNumber(gte, type, (T) null); lowerBoundType = BoundType.CLOSED; } if(lt != null) { upperBound = parseNumber(lt, type, (T) null); upperBoundType = BoundType.OPEN; } else { upperBound = parseNumber(lte, type, (T) null); upperBoundType = BoundType.CLOSED; } if(lowerBound == null) { if(upperBound == null) { return def; } else { return Range.upTo(upperBound, upperBoundType); } } else { if(upperBound == null) { return Range.downTo(lowerBound, lowerBoundType); } else { return Range.range(lowerBound, lowerBoundType, upperBound, upperBoundType); } } } public static DurationProperty parseDuration(Element parent, String name) { return durationFactory.property(parent, name); } public static Duration parseDuration(@Nullable Node node, Duration def) throws InvalidXMLException { if(node == null) return def; try { return TimeUtils.parseDuration(node.getValueNormalize()); } catch(DateTimeParseException e) { throw new InvalidXMLException("Invalid time format", node, e); } } public static @Nullable Duration parseDuration(Node node) throws InvalidXMLException { return parseDuration(node, null); } public static Duration parseDuration(Element el, Duration def) throws InvalidXMLException { return parseDuration(Node.fromNullable(el), def); } public static Duration parseDuration(Attribute attr, Duration def) throws InvalidXMLException { return parseDuration(Node.fromNullable(attr), def); } public static Duration parseDuration(Attribute attr) throws InvalidXMLException { return parseDuration(attr, null); } public static Duration parseTickDuration(Node node, String text) throws InvalidXMLException { if("oo".equals(text)) return TimeUtils.INF_POSITIVE; try { return Duration.ofMillis(Integer.parseInt(text) * 50); } catch(NumberFormatException e) { return parseDuration(node); } } public static Duration parseTickDuration(Node node) throws InvalidXMLException { return parseTickDuration(node, node.getValueNormalize()); } public static Duration parseTickDuration(Node node, Duration def) throws InvalidXMLException { return node == null ? def : parseDuration(node); } public static Duration parseSecondDuration(Node node, String text) throws InvalidXMLException { if("oo".equals(text)) return TimeUtils.INF_POSITIVE; try { return Duration.ofSeconds(Integer.parseInt(text)); } catch(NumberFormatException e) { return parseDuration(node); } } public static Duration parseSecondDuration(Node node) throws InvalidXMLException { return parseSecondDuration(node, node.getValueNormalize()); } public static Duration parseSecondDuration(Node node, Duration def) throws InvalidXMLException { return node == null ? def : parseSecondDuration(node); } public static Key parseKey(Node node, String text) throws InvalidXMLException { return Bukkit.key(text); } public static Key parseKey(Node node) throws InvalidXMLException { return parseKey(node, node.getValueNormalize()); } public static Key parseKey(Node node, Key def) throws InvalidXMLException { return node == null ? def : parseKey(node); } public static Class<? extends Entity> parseEntityType(Element el) throws InvalidXMLException { return parseEntityType(new Node(el)); } public static Class<? extends Entity> parseEntityTypeAttribute(Element el, String attributeName, Class<? extends Entity> def) throws InvalidXMLException { Node node = Node.fromAttr(el, attributeName); return node == null ? def : parseEntityType(node); } public static Class<? extends Entity> parseEntityType(Node node) throws InvalidXMLException { return parseEntityType(node, node.getValue()); } public static Class<? extends Entity> parseEntityType(Node node, String value) throws InvalidXMLException { if(!value.matches("[a-zA-Z0-9_]+")) { throw new InvalidXMLException("Invalid entity type '" + value + "'", node); } try { return Class.forName("org.bukkit.entity." + value).asSubclass(Entity.class); } catch(ClassNotFoundException | ClassCastException e) { throw new InvalidXMLException("Invalid entity type '" + value + "'", node); } } public static <T extends Number> PropertyBuilder<ImVector, ?> parseVector(Element el, String name, Class<T> type) { return new PropertyBuilder<>(el, name, VectorParser.get(type)); } public static <T extends Number> PropertyBuilder<ImVector, ?> parseVector(Element el, String name) { return parseVector(el, name, Double.class); } public static Vector parseVector(Node node, String value) throws InvalidXMLException { if(node == null) return null; String[] components = value.trim().split("\\s*,\\s*"); if(components.length != 3) { throw new InvalidXMLException("Invalid vector format", node); } try { return new Vector(parseNumber(node, components[0], Double.class, true), parseNumber(node, components[1], Double.class, true), parseNumber(node, components[2], Double.class, true)); } catch(NumberFormatException e) { throw new InvalidXMLException("Invalid vector format", node); } } public static Vector parseVector(Node node) throws InvalidXMLException { return node == null ? null : parseVector(node, node.getValue()); } public static Vector parseVector(Node node, Vector def) throws InvalidXMLException { return node == null ? def : parseVector(node); } public static Vector parseVector(Attribute attr, String value) throws InvalidXMLException { return attr == null ? null : parseVector(new Node(attr), value); } public static Vector parseVector(Attribute attr) throws InvalidXMLException { return attr == null ? null : parseVector(attr, attr.getValue()); } public static Vector parseVector(Attribute attr, Vector def) throws InvalidXMLException { return attr == null ? def : parseVector(attr); } public static Vector parse2DVector(Node node, String value) throws InvalidXMLException { String[] components = value.trim().split("\\s*,\\s*"); if(components.length != 2) { throw new InvalidXMLException("Invalid 2D vector format", node); } try { return new Vector(parseNumber(node, components[0], Double.class, true), 0d, parseNumber(node, components[1], Double.class, true)); } catch(NumberFormatException e) { throw new InvalidXMLException("Invalid 2D vector format", node); } } public static Vector parse2DVector(Node node) throws InvalidXMLException { return parse2DVector(node, node.getValue()); } public static Vector parse2DVector(Node node, Vector def) throws InvalidXMLException { return node == null ? def : parse2DVector(node); } public static BlockVector parseBlockVector(Node node, BlockVector def) throws InvalidXMLException { if(node == null) return def; String[] components = node.getValue().trim().split("\\s*,\\s*"); if(components.length != 3) { throw new InvalidXMLException("Invalid block location", node); } try { return new BlockVector(Integer.parseInt(components[0]), Integer.parseInt(components[1]), Integer.parseInt(components[2])); } catch(NumberFormatException e) { throw new InvalidXMLException("Invalid block location", node); } } public static BlockVector parseBlockVector(Node node) throws InvalidXMLException { return parseBlockVector(node, null); } public static NumericModifier parseNumericModifier(Element el) throws InvalidXMLException { return new NumericModifier(parseNumber(el.getAttribute("add"), Double.class, 0d), parseNumber(el.getAttribute("mul"), Double.class, 0d)); } public static NumericModifier parseNumericModifier(@Nullable Element el, NumericModifier def) throws InvalidXMLException { return el == null ? def : parseNumericModifier(el); } public static DyeColor parseDyeColor(Attribute attr) throws InvalidXMLException { String name = attr.getValue().replace(" ", "_").toUpperCase(); try { return DyeColor.valueOf(name); } catch(IllegalArgumentException e) { throw new InvalidXMLException("Invalid dye color '" + attr.getValue() + "'", attr); } } public static DyeColor parseDyeColor(Attribute attr, DyeColor def) throws InvalidXMLException { return attr == null ? def : parseDyeColor(attr); } public static Material parseMaterial(Node node, String text) throws InvalidXMLException { Material material = Material.matchMaterial(text); if(material == null) { throw new InvalidXMLException("Unknown material '" + text + "'", node); } return material; } public static Material parseMaterial(Node node) throws InvalidXMLException { return parseMaterial(node, node.getValueNormalize()); } public static Material parseBlockMaterial(Node node, String text) throws InvalidXMLException { Material material = parseMaterial(node, text); if(!material.isBlock()) { throw new InvalidXMLException("Material " + material.name() + " is not a block", node); } return material; } public static Material parseBlockMaterial(Node node) throws InvalidXMLException { return node == null ? null : parseBlockMaterial(node, node.getValueNormalize()); } public static MaterialData parseMaterialData(Node node, String text) throws InvalidXMLException { String[] pieces = text.split(":"); Material material = parseMaterial(node, pieces[0]); byte data; if(pieces.length > 1) { data = parseNumber(node, pieces[1], Byte.class); } else { data = 0; } return material.getNewData(data); } public static MaterialData parseMaterialData(Node node, MaterialData def) throws InvalidXMLException { return node == null ? def : parseMaterialData(node, node.getValueNormalize()); } public static MaterialData parseMaterialData(Node node) throws InvalidXMLException { return parseMaterialData(node, (MaterialData) null); } public static MaterialData parseBlockMaterialData(Node node, String text) throws InvalidXMLException { if(node == null) return null; MaterialData material = parseMaterialData(node, text); if(!material.getItemType().isBlock()) { throw new InvalidXMLException("Material " + material.getItemType().name() + " is not a block", node); } return material; } public static MaterialData parseBlockMaterialData(Node node, MaterialData def) throws InvalidXMLException { return node == null ? def : parseBlockMaterialData(node, node.getValueNormalize()); } public static MaterialData parseBlockMaterialData(Node node) throws InvalidXMLException { return parseBlockMaterialData(node, (MaterialData) null); } public static MaterialPattern parseMaterialPattern(Node node, String value) throws InvalidXMLException { try { return MaterialPattern.parse(value); } catch(IllegalArgumentException e) { throw new InvalidXMLException(e.getMessage(), node); } } public static MaterialPattern parseMaterialPattern(Node node) throws InvalidXMLException { return parseMaterialPattern(node, node.getValue()); } public static MaterialPattern parseMaterialPattern(Node node, MaterialPattern def) throws InvalidXMLException { return node == null ? def : parseMaterialPattern(node); } public static MaterialPattern parseMaterialPattern(Element el) throws InvalidXMLException { return parseMaterialPattern(new Node(el)); } public static MaterialPattern parseMaterialPattern(Attribute attr) throws InvalidXMLException { return parseMaterialPattern(new Node(attr)); } public static ImmutableSet<MaterialPattern> parseMaterialPatternSet(Node node) throws InvalidXMLException { ImmutableSet.Builder<MaterialPattern> patterns = ImmutableSet.builder(); for(String value : Splitter.on(";").split(node.getValue())) { patterns.add(parseMaterialPattern(node, value)); } return patterns.build(); } public static MaterialMatcher parseMaterialMatcher(Element el) throws InvalidXMLException { return parseMaterialMatcher(el, NoMaterialMatcher.INSTANCE); } public static MaterialMatcher parseMaterialMatcher(Element el, MaterialMatcher empty) throws InvalidXMLException { Set<MaterialMatcher> matchers = new HashSet<>(); final Attribute attrMaterial = el.getAttribute("material"); if(attrMaterial != null) { matchers.add(parseMaterialPattern(attrMaterial)); } for(Element elChild : el.getChildren()) { switch(elChild.getName()) { case "all-materials": case "all-items": return AllMaterialMatcher.INSTANCE; case "all-blocks": matchers.add(BlockMaterialMatcher.INSTANCE); break; case "material": case "item": matchers.add(parseMaterialPattern(elChild)); break; default: throw new InvalidXMLException("Unknown material matcher tag", elChild); } } return CompoundMaterialMatcher.of(matchers, empty); } public static PotionEffectType parsePotionEffectType(Node node) throws InvalidXMLException { return parsePotionEffectType(node, node.getValue()); } public static PotionEffectType parsePotionEffectType(Node node, String text) throws InvalidXMLException { PotionEffectType type = PotionEffectType.getByName(text.toUpperCase().replace(" ", "_")); if(type == null) type = Bukkit.potionEffectRegistry().get(parseKey(node, text)); if(type == null) { throw new InvalidXMLException("Unknown potion effect '" + node.getValue() + "'", node); } return type; } private static PotionEffect createPotionEffect(Node node, PotionEffectType type, Duration duration, int amplifier, boolean ambient) throws InvalidXMLException { if(PotionEffectType.HEALTH_BOOST.equals(type) && amplifier <= -6) { // HB -5 kills the player instantly and sets their max health to 0, // and we don't seem to be able to change it before the vanilla death screen shows. throw new InvalidXMLException("Health boost level -5 and below is not supported", node); } return new PotionEffect(type, TimeUtils.isInfPositive(duration) ? Integer.MAX_VALUE : (int) (duration.toMillis() / 50), Duration.ZERO.equals(duration) ? 0 : amplifier, // negative amplifier may cause an error, but we can ignore it if duration is zero ambient); } public static PotionEffect parsePotionEffect(Element el) throws InvalidXMLException { Node node = new Node(el); PotionEffectType type = parsePotionEffectType(node); Duration duration = parseSecondDuration(Node.fromAttr(el, "duration"), TimeUtils.INF_POSITIVE); int amplifier = parseNumber(Node.fromAttr(el, "amplifier"), Integer.class, 1) - 1; boolean ambient = parseBoolean(Node.fromAttr(el, "ambient"), false); return createPotionEffect(node, type, duration, amplifier, ambient); } public static PotionEffect parseCompactPotionEffect(Node node, String text) throws InvalidXMLException { String[] parts = text.split(":"); if(parts.length == 0) throw new InvalidXMLException("Missing potion effect", node); PotionEffectType type = parsePotionEffectType(node, parts[0]); Duration duration = TimeUtils.INF_POSITIVE; int amplifier = 0; boolean ambient = false; if(parts.length >= 2) { duration = parseTickDuration(node, parts[1]); if(parts.length >= 3) { amplifier = parseNumber(node, parts[2], Integer.class); if(parts.length >= 4) { ambient = parseBoolean(node, parts[3]); } } } return createPotionEffect(node, type, duration, amplifier, ambient); } public static PotionBrew parsePotion(Node node, String text) throws InvalidXMLException { final PotionBrew brew = Bukkit.potionRegistry().get(parseKey(node, text)); if(brew == null) { throw new InvalidXMLException("Unknown potion '" + text + "'", node); } return brew; } public static PotionBrew parsePotion(Node node) throws InvalidXMLException { return parsePotion(node, node.getValueNormalize()); } public static PotionBrew parsePotion(Node node, PotionBrew def) throws InvalidXMLException { return node == null ? def : parsePotion(node); } public static PotionBrew parsePotionOrFallback(Node node) throws InvalidXMLException { final PotionBrew brew = parsePotion(node, (PotionBrew) null); return brew != null ? brew : Bukkit.potionRegistry().getFallback(); } public static <T extends Enum<T>> T parseEnum(Node node, String text, Class<T> type, String readableType) throws InvalidXMLException { text = text.trim().replace(' ', '_'); try { // First, try the fast way return Enum.valueOf(type, text); } catch(IllegalArgumentException ex) { // If that fails, search for a case-insensitive match, without assuming enums are always uppercase for(T value : type.getEnumConstants()) { if(value.name().equalsIgnoreCase(text)) return value; } throw new InvalidXMLException("Unknown " + readableType + " '" + text + "'", node); } } public static <T extends Enum<T>> T parseEnum(@Nullable Node node, Class<T> type, String readableType, @Nullable T def) throws InvalidXMLException { if(node == null) return def; return parseEnum(node, node.getValueNormalize(), type, readableType); } public static <T extends Enum<T>> T parseEnum(@Nullable Node node, Class<T> type, String readableType) throws InvalidXMLException { return parseEnum(node, type, readableType, null); } public static <T extends Enum<T>> T parseEnum(Element el, Class<T> type) throws InvalidXMLException { return parseEnum(new Node(el), type, type.getSimpleName()); } public static <T extends Enum<T>> T parseEnum(Element el, Class<T> type, String readableType) throws InvalidXMLException { return parseEnum(new Node(el), type, readableType); } public static <T extends Enum<T>> T parseEnum(Attribute attr, Class<T> type, String readableType) throws InvalidXMLException { return parseEnum(new Node(attr), type, readableType); } public static ChatColor parseChatColor(@Nullable Node node) throws InvalidXMLException { return parseEnum(node, ChatColor.class, "color"); } public static ChatColor parseChatColor(@Nullable Node node, ChatColor def) throws InvalidXMLException { return node == null ? def : parseChatColor(node); } public static String getNormalizedNullableText(Element el) { String text = el.getTextNormalize(); if(text == null || "".equals(text)) { return null; } else { return text; } } public static String getNullableAttribute(Element el, String...attrs) { String text = null; for(String attr : attrs) { text = el.getAttributeValue(attr); if(text != null) break; } return text; } public static UUID parseUuid(@Nullable Node node) throws InvalidXMLException { if(node == null) return null; String raw = node.getValue(); try { return UUID.fromString(raw); } catch(IllegalArgumentException e) { throw new InvalidXMLException("Invalid UUID format (must be 8-4-4-4-12)", node, e); } } private static final Pattern USERNAME_REGEX = Pattern.compile("[a-zA-Z0-9_]{1,16}"); public static String parseUsername(@Nullable Node node) throws InvalidXMLException { if(node == null) return null; String name = node.getValueNormalize(); if(!USERNAME_REGEX.matcher(name).matches()) { throw new InvalidXMLException("Invalid Minecraft username '" + name + "'", node); } return name; } public static Skin parseUnsignedSkin(@Nullable Node node) throws InvalidXMLException { if(node == null) return null; String data = node.getValueNormalize(); try { Base64.decodeBase64(data.getBytes()); } catch(IllegalArgumentException e) { throw new InvalidXMLException("Skin data is not valid base64", node); } return new Skin(data, null); } /** * Guess if the given text is a JSON object by looking for the curly braces at either end */ public static boolean looksLikeJson(String text) { text = text.trim(); return text.startsWith("{") && text.endsWith("}"); } /** * Parse a piece of formatted text, which can be either plain text with legacy * formatting codes, or JSON chat components. */ public static BaseComponent parseFormattedText(@Nullable Node node, BaseComponent def) throws InvalidXMLException { if(node == null) return def; // <blah translate="x"/> is shorthand for <blah>{"translate":"x"}</blah> if(node.isElement()) { final Attribute translate = node.asElement().getAttribute("translate"); if(translate != null) { return new TranslatableComponent(translate.getValue()); } } String text = node.getValueNormalize(); if(looksLikeJson(text)) { try { return Components.concat(ComponentSerializer.parse(node.getValue())); } catch(JsonParseException e) { throw new InvalidXMLException(e.getMessage(), node, e); } } else { return Components.concat(TextComponent.fromLegacyText(BukkitUtils.colorize(text))); } } /** * Parse a piece of formatted text, which can be either plain text with legacy * formatting codes, or JSON chat components. */ public static @Nullable BaseComponent parseFormattedText(@Nullable Node node) throws InvalidXMLException { return parseFormattedText(node, null); } /** * Parse a piece of formatted text, which can be either plain text with legacy * formatting codes, or JSON chat components. */ public static BaseComponent parseFormattedText(Element parent, String property, BaseComponent def) throws InvalidXMLException { return parseFormattedText(Node.fromChildOrAttr(parent, property), def); } /** * Parse a piece of formatted text, which can be either plain text with legacy * formatting codes, or JSON chat components. */ public static BaseComponent parseFormattedText(Element parent, String property) throws InvalidXMLException { return parseFormattedText(Node.fromChildOrAttr(parent, property)); } public static BaseComponent parseLocalizedText(Element el) throws InvalidXMLException { final Attribute translate = el.getAttribute("translate"); if(translate != null) { return new TranslatableComponent(translate.getValue()); } else { return new Component(el.getTextNormalize()); } } public static BaseComponent parseLocalizedText(@Nullable Element el, BaseComponent def) throws InvalidXMLException { return el == null ? def : parseLocalizedText(el); } public static BaseComponent parseLocalizedText(Node node) throws InvalidXMLException { if(node.isElement()) { return parseLocalizedText(node.asElement()); } else { return new Component(node.getValueNormalize()); } } public static PropertyBuilder<String, ?> parseMessageKey(Element el, String name) throws InvalidXMLException { return new PropertyBuilder<>(el, name, StringParser.get()) .validate((value, node) -> { if(!Translations.get().hasKey(value)) { throw new InvalidXMLException("Unknown message key '" + value + "'", node); } }); } public static PropertyBuilder<Team.OptionStatus, ?> parseNameTagVisibility(Element parent, String name) throws InvalidXMLException { return new PropertyBuilder<>(parent, name, new TeamRelationParser()); } public static Enchantment parseEnchantment(Node node) throws InvalidXMLException { return parseEnchantment(node, node.getValueNormalize()); } public static Enchantment parseEnchantment(Node node, String text) throws InvalidXMLException { Enchantment enchantment = Enchantment.getByName(text.toUpperCase().replace(" ", "_")); if(enchantment == null) enchantment = NMSHacks.getEnchantment(text); if(enchantment == null) { throw new InvalidXMLException("Unknown enchantment '" + text + "'", node); } return enchantment; } public static org.bukkit.attribute.Attribute parseAttribute(Node node, String text) throws InvalidXMLException { return attributeParser.parse(node, text); } public static org.bukkit.attribute.Attribute parseAttribute(Node node) throws InvalidXMLException { return parseAttribute(node, node.getValueNormalize()); } public static AttributeModifier.Operation parseAttributeOperation(Node node, String text) throws InvalidXMLException { switch(text.toLowerCase()) { case "add": return AttributeModifier.Operation.ADD_NUMBER; case "base": return AttributeModifier.Operation.ADD_SCALAR; case "multiply": return AttributeModifier.Operation.MULTIPLY_SCALAR_1; } throw new InvalidXMLException("Unknown attribute modifier operation '" + text + "'", node); } public static AttributeModifier.Operation parseAttributeOperation(Node node) throws InvalidXMLException { return parseAttributeOperation(node, node.getValueNormalize()); } public static AttributeModifier.Operation parseAttributeOperation(Node node, AttributeModifier.Operation def) throws InvalidXMLException { return node == null ? def : parseAttributeOperation(node); } public static Pair<org.bukkit.attribute.Attribute, AttributeModifier> parseCompactAttributeModifier(Node node, String text) throws InvalidXMLException { final String[] parts = text.split(":"); if(parts.length != 3) { throw new InvalidXMLException("Bad attribute modifier format", node); } return Pair.create( parseAttribute(node, parts[0]), new AttributeModifier( "FromXML", parseNumber(node, parts[2], Double.class), parseAttributeOperation(node, parts[1]) ) ); } public static Pair<org.bukkit.attribute.Attribute, AttributeModifier> parseAttributeModifier(Element el) throws InvalidXMLException { return Pair.create( parseAttribute(new Node(el)), new AttributeModifier( "FromXML", parseNumber(Node.fromRequiredAttr(el, "amount"), Double.class), parseAttributeOperation(Node.fromAttr(el, "operation"), AttributeModifier.Operation.ADD_NUMBER) ) ); } public static Pair<org.bukkit.attribute.Attribute, ItemAttributeModifier> parseItemAttributeModifier(Element el) throws InvalidXMLException { return Pair.create( parseAttribute(new Node(el)), new ItemAttributeModifier( parseEquipmentSlot(Node.fromAttr(el, "slot"), null), parseAttributeModifier(el).second ) ); } public static GameMode parseGameMode(Node node, String text) throws InvalidXMLException { text = text.trim(); try { return GameMode.valueOf(text.toUpperCase()); } catch(IllegalArgumentException e) { throw new InvalidXMLException("Unknown game-mode '" + text + "'", node); } } public static GameMode parseGameMode(Node node) throws InvalidXMLException { return parseGameMode(node, node.getValueNormalize()); } public static GameMode parseGameMode(Node node, GameMode def) throws InvalidXMLException { return node == null ? def : parseGameMode(node); } public static Path parseRelativePath(Node node) throws InvalidXMLException { return parseRelativePath(node, null); } public static Path parseRelativePath(Node node, Path def) throws InvalidXMLException { if(node == null) return def; final String text = node.getValueNormalize(); try { Path path = Paths.get(text); if(path.isAbsolute()) { throw new InvalidPathException(text, "Path is not relative"); } for(Path part : path) { if(part.toString().trim().startsWith("..")) { throw new InvalidPathException(text, "Path contains an invalid component"); } } return path; } catch(InvalidPathException e) { throw new InvalidXMLException("Invalid relative path '" + text + "'", node, e); } } public static Path parseRelativePath(Path basePath, Node node, Path def) throws InvalidXMLException { if(node == null) return def; final Path path; try { path = basePath.resolve(node.getValue()).toRealPath(); } catch(IOException e) { throw new InvalidXMLException("Error resolving relative file path", node); } if(!path.startsWith(basePath)) { throw new InvalidXMLException("Invalid relative file path", node); } return path; } public static Path parseRelativeFolder(Path basePath, Node node, Path def) throws InvalidXMLException { Path path = parseRelativePath(basePath, node, def); if(!Objects.equals(path, def) && !Files.isDirectory(path)) { throw new InvalidXMLException("Folder does not exist", node); } return path; } public static SemanticVersion parseSemanticVersion(Node node) throws InvalidXMLException { if(node == null) return null; String[] parts = node.getValueNormalize().split("\\.", 3); if(parts.length < 1 || parts.length > 3) { throw new InvalidXMLException("Version must be 1 to 3 whole numbers, separated by periods", node); } int major = parseNumber(node, parts[0], Integer.class); int minor = parts.length < 2 ? 0 : parseNumber(node, parts[1], Integer.class); int patch = parts.length < 3 ? 0 : parseNumber(node, parts[2], Integer.class); return new SemanticVersion(major, minor, patch); } public static Slot.Player parsePlayerSlot(Node node) throws InvalidXMLException { String value = node.getValue(); Slot slot; try { slot = Slot.Player.forIndex(Integer.parseInt(value)); if(slot == null) { throw new InvalidXMLException("Invalid inventory slot index (must be between 0 and 35)", node); } } catch(NumberFormatException e) { slot = Slot.forKey(value); if(slot == null) { throw new InvalidXMLException("Invalid inventory slot name", node); } } if(slot instanceof Slot.Player) { return (Slot.Player) slot; } throw new InvalidXMLException(value + " is not a player slot", node); } public static EquipmentSlot parseEquipmentSlot(Node node, EquipmentSlot def) throws InvalidXMLException { return node == null ? def : parseEquipmentSlot(node); } public static EquipmentSlot parseEquipmentSlot(Node node) throws InvalidXMLException { final EquipmentSlot slot = parsePlayerSlot(node).toEquipmentSlot(); if(slot == null) { throw new InvalidXMLException("Not an equipment slot", node); } return slot; } /** * Return the path of the given Element in its Document as a list of indexes * relative to parent elements. * * The root element of a document has an empty path. */ public static TIntList indexPath(Element el) { return indexPath(el, 0); } private static TIntList indexPath(Element child, int size) { final Element parent = child.getParentElement(); if(parent == null) { return new TIntArrayList(size); } else { final TIntList path = indexPath(parent, size + 1); final int index = ((BoundedElement) child).indexInParent(); if(index < 0) { throw new IllegalStateException("Parent element " + parent + " does not contain its child element " + child); } path.add(index); return path; } } }