/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.text.selector; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.game.registry.type.text.SelectorTypeRegistryModule; import org.spongepowered.api.Sponge; import org.spongepowered.api.scoreboard.Score; import org.spongepowered.api.text.selector.Argument; import org.spongepowered.api.text.selector.ArgumentHolder; import org.spongepowered.api.text.selector.ArgumentHolder.Limit; import org.spongepowered.api.text.selector.ArgumentType; import org.spongepowered.api.text.selector.ArgumentTypes; import org.spongepowered.api.text.selector.Selector; import org.spongepowered.api.text.selector.SelectorFactory; import org.spongepowered.api.text.selector.SelectorType; import org.spongepowered.api.util.GuavaCollectors; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import javax.annotation.Nullable; public class LanternSelectorFactory implements SelectorFactory { private static final Pattern intListPattern = Pattern.compile("\\G([-!]?[\\w-]*)(?:$|,)"); private static final Pattern keyValueListPattern = Pattern.compile("\\G(\\w+)=([-!]?[\\w-]*)(?:$|,)"); private final static String argumentNamesLookup = "xyzr"; public static <K, V> Function<K, V> methodAsFunction(final Method m, boolean isStatic) { if (isStatic) { return input -> { try { return (V) m.invoke(null, input); } catch (IllegalAccessException e) { Lantern.getLogger().debug(m + " wasn't public", e); return null; } catch (IllegalArgumentException e) { Lantern.getLogger().debug(m + " failed with parameter " + input, e); return null; } catch (InvocationTargetException e) { throw Throwables.propagate(e.getCause()); } }; } else { return input -> { try { return (V) m.invoke(input); } catch (IllegalAccessException e) { Lantern.getLogger().debug(m + " wasn't public", e); return null; } catch (IllegalArgumentException e) { Lantern.getLogger().debug(m + " failed with parameter " + input, e); return null; } catch (InvocationTargetException e) { throw Throwables.propagate(e.getCause()); } }; } } @SuppressWarnings("unchecked") private static <T> Optional<T> recast(Optional<?> source) { return (Optional<T>) source; } private final Map<String, ArgumentHolder.Limit<ArgumentType<Integer>>> scoreToTypeMap = Maps.newLinkedHashMap(); private final Map<String, ArgumentType<?>> argumentLookupMap = Maps.newLinkedHashMap(); private final SelectorTypeRegistryModule selectorTypeRegistry; public LanternSelectorFactory(SelectorTypeRegistryModule selectorTypeRegistry) { this.selectorTypeRegistry = selectorTypeRegistry; } @Override public Selector.Builder createBuilder() { return new LanternSelectorBuilder(); } @Override public Selector parseRawSelector(String selector) { checkArgument(selector.startsWith("@"), "Invalid selector %s", selector); // If multi-character types are possible, this handles it int argListIndex = selector.indexOf('['); if (argListIndex < 0) { argListIndex = selector.length(); } else { int end = selector.indexOf(']'); checkArgument(end > argListIndex && selector.charAt(end - 1) != ',', "Invalid selector %s", selector); } String typeStr = selector.substring(1, argListIndex); Optional<SelectorType> optSelectorType = this.selectorTypeRegistry.getById(typeStr); checkArgument(optSelectorType.isPresent(), "No type known as '%s'", typeStr); try { Map<String, String> rawMap; if (argListIndex == selector.length()) { rawMap = ImmutableMap.of(); } else { rawMap = this.parseArgumentsMap(selector.substring(argListIndex + 1, selector.length() - 1)); } Map<ArgumentType<?>, Argument<?>> arguments = parseArguments(rawMap); return new LanternSelector(optSelectorType.get(), ImmutableMap.copyOf(arguments)); } catch (Exception e) { throw new IllegalArgumentException("Invalid selector " + selector, e); } } @Override public Limit<ArgumentType<Integer>> createScoreArgumentType(String name) { if (!this.scoreToTypeMap.containsKey(name)) { LanternArgumentType<Integer> min = this.createArgumentType("score_" + name + "_min", Integer.class, Score.class.getName()); LanternArgumentType<Integer> max = this.createArgumentType("score_" + name, Integer.class, Score.class.getName()); this.scoreToTypeMap.put(name, new LanternArgumentHolder.LanternLimit<>(min, max)); } return this.scoreToTypeMap.get(name); } @Override public Optional<ArgumentType<?>> getArgumentType(String name) { return recast(Optional.ofNullable(this.argumentLookupMap.get(name))); } @Override public Collection<ArgumentType<?>> getArgumentTypes() { return this.argumentLookupMap.values(); } @Override public LanternArgumentType<String> createArgumentType(String key) { return this.createArgumentType(key, String.class); } @Override public <T> LanternArgumentType<T> createArgumentType(String key, Class<T> type) { return this.createArgumentType(key, type, type.getName()); } @SuppressWarnings("unchecked") public <T> LanternArgumentType<T> createArgumentType(String key, Class<T> type, String converterKey) { if (!this.argumentLookupMap.containsKey(key)) { this.argumentLookupMap.put(key, new LanternArgumentType<>(key, type, converterKey)); } return (LanternArgumentType<T>) this.argumentLookupMap.get(key); } public LanternArgumentType.Invertible<String> createInvertibleArgumentType(String key) { return this.createInvertibleArgumentType(key, String.class); } public <T> LanternArgumentType.Invertible<T> createInvertibleArgumentType(String key, Class<T> type) { return this.createInvertibleArgumentType(key, type, type.getName()); } @SuppressWarnings("unchecked") public <T> LanternArgumentType.Invertible<T> createInvertibleArgumentType(String key, Class<T> type, String converterKey) { if (!this.argumentLookupMap.containsKey(key)) { this.argumentLookupMap.put(key, new LanternArgumentType.Invertible<>(key, type, converterKey)); } return (LanternArgumentType.Invertible<T>) this.argumentLookupMap.get(key); } @Override public <T> Argument<T> createArgument(ArgumentType<T> type, T value) { if (type instanceof ArgumentType.Invertible) { return this.createArgument((ArgumentType.Invertible<T>) type, value, false); } return new LanternArgument<>(type, value); } @Override public <T> Argument.Invertible<T> createArgument(ArgumentType.Invertible<T> type, T value, boolean inverted) { return new LanternArgument.Invertible<>(type, value, inverted); } @SuppressWarnings("unchecked") @Override public <T, V> Set<Argument<T>> createArguments( ArgumentHolder<? extends ArgumentType<T>> type, V value) { Set<Argument<T>> set = Sets.newLinkedHashSet(); if (type instanceof LanternArgumentHolder.LanternVector3) { Set<Function<V, T>> extractors = ((LanternArgumentHolder.LanternVector3<V, T>) type).extractFunctions(); Set<? extends ArgumentType<T>> types = type.getTypes(); Iterator<Function<V, T>> extIter = extractors.iterator(); Iterator<? extends ArgumentType<T>> typeIter = types.iterator(); while (extIter.hasNext() && typeIter.hasNext()) { Function<V, T> extractor = extIter.next(); ArgumentType<T> subtype = typeIter.next(); set.add(createArgument(subtype, extractor.apply(value))); } } return set; } @Override public Argument<?> parseArgument(String argument) throws IllegalArgumentException { String[] argBits = argument.split("="); LanternArgumentType<Object> type = this.getArgumentTypeWithChecks(argBits[0]); return this.parseArgumentCreateShared(type, argBits[1]); } public Map<ArgumentType<?>, Argument<?>> parseArguments(Map<String, String> argumentMap) { Map<ArgumentType<?>, Argument<?>> generated = Maps.newHashMapWithExpectedSize(argumentMap.size()); for (Entry<String, String> argument : argumentMap.entrySet()) { String argKey = argument.getKey(); LanternArgumentType<Object> type = this.getArgumentTypeWithChecks(argKey); String value = argument.getValue(); generated.put(type, this.parseArgumentCreateShared(type, value)); } return generated; } public Map<String, String> parseArgumentsMap(@Nullable String input) { Map<String, String> map = Maps.newHashMap(); if (input == null) { return map; } int index = 0; int end = -1; Matcher matcher = intListPattern.matcher(input); while (matcher.find()) { if (argumentNamesLookup.length() < index++ && matcher.group(1).length() > 0) { map.put(argumentNamesLookup.indexOf(index - 1) + "", matcher.group(2)); } end = matcher.end(); } if (end < input.length()) { matcher = keyValueListPattern.matcher(end == -1 ? input : input.substring(end)); while (matcher.find()) { map.put(matcher.group(1), matcher.group(2)); } } return map; } @SuppressWarnings("unchecked") private LanternArgumentType<Object> getArgumentTypeWithChecks(String argKey) { Optional<ArgumentType<?>> type = ArgumentTypes.valueOf(argKey); if (!type.isPresent()) { throw new IllegalArgumentException("Invalid argument key " + argKey); } ArgumentType<Object> unwrappedType = (ArgumentType<Object>) type.get(); if (!(unwrappedType instanceof LanternArgumentType)) { // TODO handle convert generally? throw new IllegalStateException("Cannot convert from string: " + unwrappedType); } return (LanternArgumentType<Object>) unwrappedType; } @SuppressWarnings("unchecked") private Argument<?> parseArgumentCreateShared(LanternArgumentType<Object> type, String value) { Argument<?> created; if (type instanceof ArgumentType.Invertible && value.charAt(0) == '!') { created = this.createArgument((ArgumentType.Invertible<Object>) type, type.convert(value.substring(1)), true); } else { created = this.createArgument(type, type.convert(value)); } return created; } @Override public List<String> complete(String selector) { if (!selector.startsWith("@") || selector.contains("]")) { return ImmutableList.of(); } Stream<String> choices; if (!selector.contains("[")) { // No arguments yet choices = Sponge.getRegistry().getAllOf(SelectorType.class).stream().map(type -> "@" + type.getName()); } else { int keyStart = Math.max(selector.indexOf("["), selector.lastIndexOf(",")) + 1; int valueStart = selector.lastIndexOf("=") + 1; final String prefix = selector.substring(Math.max(keyStart, valueStart)); if (keyStart > valueStart) { // Tab completing key choices = ArgumentTypes.values().stream().map(ArgumentType::getKey); } else { // Tab completing value Optional<ArgumentType<?>> type = ArgumentTypes.valueOf(selector.substring(keyStart, valueStart - 1)); if (!type.isPresent()) { return ImmutableList.of(); } // TODO How to get all the values of an argument type? return ImmutableList.of(); } choices = choices.map(input -> prefix + input); } return choices.filter(choice -> choice.startsWith(selector)).collect(GuavaCollectors.toImmutableList()); } }