001//
002// MIT License
003//
004// Copyright (c) 2021 Alexander Söderberg & Contributors
005//
006// Permission is hereby granted, free of charge, to any person obtaining a copy
007// of this software and associated documentation files (the "Software"), to deal
008// in the Software without restriction, including without limitation the rights
009// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010// copies of the Software, and to permit persons to whom the Software is
011// furnished to do so, subject to the following conditions:
012//
013// The above copyright notice and this permission notice shall be included in all
014// copies or substantial portions of the Software.
015//
016// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022// SOFTWARE.
023//
024package cloud.commandframework.meta;
025
026import io.leangen.geantyref.GenericTypeReflector;
027import org.checkerframework.checker.nullness.qual.NonNull;
028
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.Map;
032import java.util.Objects;
033import java.util.Optional;
034import java.util.stream.Collectors;
035
036/**
037 * A simple immutable string-string map containing command meta
038 */
039@SuppressWarnings("unused")
040public class SimpleCommandMeta extends CommandMeta {
041
042    private final Map<String, Object> metaMap;
043
044    @Deprecated
045    protected SimpleCommandMeta(final @NonNull Map<@NonNull String, @NonNull String> metaMap) {
046        this.metaMap = Collections.unmodifiableMap(metaMap);
047    }
048
049    protected SimpleCommandMeta(final SimpleCommandMeta source) {
050        this.metaMap = source.metaMap;
051    }
052
053    // Constructor needs an extra flag to distinguish it from the old one (for reified generics)
054    SimpleCommandMeta(final @NonNull Map<@NonNull String, @NonNull Object> metaMap, final boolean unusedMarkerForNew) {
055        this.metaMap = Collections.unmodifiableMap(metaMap);
056    }
057
058    /**
059     * Create a new meta builder
060     *
061     * @return Builder instance
062     */
063    public static SimpleCommandMeta.@NonNull Builder builder() {
064        return new Builder();
065    }
066
067    /**
068     * Create an empty simple command meta instance
069     *
070     * @return Empty instance
071     */
072    public static @NonNull SimpleCommandMeta empty() {
073        return SimpleCommandMeta.builder().build();
074    }
075
076    @Override
077    @Deprecated
078    public final @NonNull Optional<String> getValue(final @NonNull String key) {
079        final Object result = this.metaMap.get(key);
080        if (result != null && !(result instanceof String)) {
081            throw new IllegalArgumentException("Key '" + key + "' has been used for a new typed command meta and contains a "
082                    + "non-string value!");
083        }
084        return Optional.ofNullable((String) result);
085    }
086
087    @Override
088    @Deprecated
089    public final @NonNull String getOrDefault(final @NonNull String key, final @NonNull String defaultValue) {
090        return this.getValue(key).orElse(defaultValue);
091    }
092
093    @Override
094    @SuppressWarnings("unchecked")
095    public final @NonNull <V> Optional<V> get(final @NonNull Key<V> key) {
096        final Object value = this.metaMap.get(key.getName());
097        if (value == null) {
098            // Attempt to use a fallback legacy type
099            if (key.getFallbackDerivation() != null) {
100                return Optional.ofNullable(key.getFallbackDerivation().apply(this));
101            }
102
103            return Optional.empty();
104        }
105        if (!GenericTypeReflector.isSuperType(key.getValueType().getType(), value.getClass())) {
106            throw new IllegalArgumentException("Conflicting argument types between key type of "
107                    + key.getValueType().getType() + " and value type of " + value.getClass());
108        }
109
110        return Optional.of((V) value);
111    }
112
113    @Override
114    public final <V> @NonNull V getOrDefault(final @NonNull Key<V> key, final @NonNull V defaultValue) {
115        return this.get(key).orElse(defaultValue);
116    }
117
118    @Override
119    @Deprecated
120    public final @NonNull Map<@NonNull String, @NonNull String> getAll() {
121        return this.metaMap.entrySet()
122                .stream().filter(ent -> ent.getValue() instanceof String)
123                .collect(Collectors.<Map.Entry<String, Object>, String, String>toMap(
124                        Map.Entry::getKey,
125                        ent -> ent.getValue().toString()
126                ));
127    }
128
129    @Override
130    public final @NonNull Map<@NonNull String, @NonNull ?> getAllValues() {
131        return new HashMap<>(this.metaMap);
132    }
133
134    @Override
135    public final boolean equals(final Object other) {
136        if (this == other) {
137            return true;
138        }
139        if (other == null || getClass() != other.getClass()) {
140            return false;
141        }
142        final SimpleCommandMeta that = (SimpleCommandMeta) other;
143        return Objects.equals(this.metaMap, that.metaMap);
144    }
145
146    @Override
147    public final int hashCode() {
148        return Objects.hashCode(this.metaMap);
149    }
150
151    /**
152     * Builder for {@link SimpleCommandMeta}
153     */
154    public static final class Builder {
155
156        private final Map<String, Object> map = new HashMap<>();
157
158        private Builder() {
159        }
160
161        /**
162         * Copy all values from another command meta instance
163         *
164         * @param commandMeta Existing instance
165         * @return Builder instance
166         */
167        public @NonNull Builder with(final @NonNull CommandMeta commandMeta) {
168            if (commandMeta instanceof SimpleCommandMeta) {
169                this.map.putAll(((SimpleCommandMeta) commandMeta).metaMap);
170            } else {
171                this.map.putAll(commandMeta.getAllValues());
172            }
173            return this;
174        }
175
176        /**
177         * Store a new key-value pair in the meta map
178         *
179         * @param key   Key
180         * @param value Value
181         * @return Builder instance
182         * @deprecated For removal since 1.3.0, use {@link #with(Key, Object) the typesafe alternative} instead
183         */
184        @Deprecated
185        public @NonNull Builder with(
186                final @NonNull String key,
187                final @NonNull String value
188        ) {
189            this.map.put(key, value);
190            return this;
191        }
192
193        /**
194         * Store a new key-value pair in the meta map
195         *
196         * @param <V>   Value type
197         * @param key   Key
198         * @param value Value
199         * @return Builder instance
200         * @since 1.3.0
201         */
202        public <V> @NonNull Builder with(
203                final @NonNull Key<V> key,
204                final @NonNull V value
205        ) {
206           this.map.put(key.getName(), value);
207           return this;
208        }
209
210        /**
211         * Construct a new meta instance
212         *
213         * @return Meta instance
214         */
215        public @NonNull SimpleCommandMeta build() {
216            return new SimpleCommandMeta(this.map, false);
217        }
218
219    }
220
221}