001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.subject;
018
019import io.leangen.geantyref.GenericTypeReflector;
020import io.leangen.geantyref.TypeToken;
021import org.checkerframework.checker.nullness.qual.Nullable;
022import org.pcollections.HashTreePMap;
023import org.pcollections.PMap;
024
025import java.util.Map;
026import java.util.function.Function;
027import java.util.function.Supplier;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * A definition for parameters controlling a {@link SubjectTypeCollection}'s handling.
033 *
034 * @param <I> identifier type
035 * @since 2.0.0
036 */
037public final class SubjectType<I> {
038    private final String name;
039    private final Class<I> identifierType;
040    private final boolean transientHasPriority;
041    private final Function<I, Boolean> undefinedValueProvider;
042    private final Function<String, I> identifierDeserializer;
043    private final Function<I, String> identifierSerializer;
044    private final Function<I, @Nullable ?> associatedObjectProvider;
045    private final Function<String, I> friendlyDeserializer;
046
047    /**
048     * Create a new builder for subject types.
049     *
050     * @param name the subject type name
051     * @param identifierType explicit type of identifiers
052     * @param <V> the identifier type
053     * @return a builder for a subject type
054     * @since 2.0.0
055     */
056    public static <V> Builder<V> builder(final String name, final Class<V> identifierType) {
057        return new Builder<>(name, identifierType);
058    }
059
060    /**
061     * Create a new builder for subject types.
062     *
063     * @param name the subject type name
064     * @param identifierType explicit type of identifiers
065     * @param <V> the identifier type
066     * @return a builder for a subject type
067     * @since 2.0.0
068     */
069    @SuppressWarnings({"unchecked", "rawtypes"})
070    public static <V> Builder<V> builder(final String name, final TypeToken<V> identifierType) {
071        return new Builder(name, GenericTypeReflector.erase(identifierType.getType()));
072    }
073
074    /**
075     * Create a new builder for subject types using a string identifier.
076     *
077     * @param name the subject type name
078     * @return a builder for a subject type
079     * @since 2.0.0
080     */
081    public static Builder<String> stringIdentBuilder(final String name) {
082        return builder(name, String.class)
083                .serializedBy(Function.identity())
084                .deserializedBy(Function.identity());
085    }
086
087    /**
088     * Create a subject type from a builder.
089     *
090     * @param builder the builder
091     */
092    SubjectType(final Builder<I> builder) {
093        this.name = builder.name;
094        this.identifierType = builder.identifierType;
095        this.transientHasPriority = builder.transientHasPriority;
096        this.undefinedValueProvider = builder.undefinedValueProvider;
097        this.associatedObjectProvider = builder.associatedObjectProvider;
098        this.friendlyDeserializer = builder.friendlyDeserializer;
099        this.identifierDeserializer = requireNonNull(builder.identifierDeserializer, "Identifier deserializer has not been provided for subject type " + this.name);
100        this.identifierSerializer = requireNonNull(builder.identifierSerializer, "Identifier serializer has not been set for subject type " + this.name);
101    }
102
103    /**
104     * The name of the subject type this defines.
105     *
106     * @return the type name
107     * @since 2.0.0
108     */
109    public final String name() {
110        return this.name;
111    }
112
113    /**
114     * Return whether or not transient data takes priority over persistent for this subject type.
115     *
116     * @return Whether or not transient data has priority.
117     * @since 2.0.0
118     */
119    public final boolean transientHasPriority() {
120        return this.transientHasPriority;
121    }
122
123    /**
124     * Check if a name is a valid identifier for a given subject collection
125     *
126     * @param serialized The identifier to check
127     * @return Whether or not the given name is a valid identifier
128     */
129    public boolean isIdentifierValid(final String serialized) {
130        try {
131            parseIdentifier(serialized);
132            return true;
133        } catch (final InvalidIdentifierException ex) {
134            return false;
135        }
136    }
137
138    /**
139     * Parse an identifier given its serialized string representation.
140     *
141     * @param input the serialized form
142     * @return a parsed identifier
143     * @throws InvalidIdentifierException if an identifier is not of appropriate format for
144     *         this subject type.
145     */
146    public I parseIdentifier(final String input) {
147        return this.identifierDeserializer.apply(requireNonNull(input, "input"));
148    }
149
150    /**
151     * Serialize an identifier to its canonical represenattion.
152     *
153     * @param input the identifier
154     * @return the canonical representation of the identifier
155     */
156    public String serializeIdentifier(final I input)  {
157        return this.identifierSerializer.apply(requireNonNull(input, "input"));
158    }
159
160    /**
161     * Attempt to parse an identifier, while also attempting to resolve from any user-friendly
162     * display name that may be available.
163     *
164     * <p>Unlike {@link #parseIdentifier(String)}, this will not throw a
165     * {@link InvalidIdentifierException} when identifiers are of an invalid format. Instead it may
166     * attempt to perform some sort of lookup to resolve an identifier from the
167     * provided information.</p>
168     *
169     * @param name The friendly name that may be used
170     * @return A standard representation of the subject identifier
171     */
172    public @Nullable I parseOrCoerceIdentifier(String name) {
173        try {
174            return this.parseIdentifier(name);
175        } catch (final InvalidIdentifierException ex) {
176            return this.friendlyDeserializer.apply(name);
177        }
178    }
179
180    /**
181     * The native object that may be held
182     *
183     * @param identifier type
184     * @return A native object that has its permissions defined by this subject
185     */
186    public @Nullable Object getAssociatedObject(I identifier) {
187        return this.associatedObjectProvider.apply(identifier);
188    }
189
190    /**
191     * The boolean value an undefined permission should have for this subject type
192     */
193    public boolean undefinedPermissionValue(final I identifier) {
194        return this.undefinedValueProvider.apply(requireNonNull(identifier));
195    }
196
197    @Override
198    public int hashCode() {
199        return 7 * this.name.hashCode()
200                + 31 * this.identifierType.hashCode();
201    }
202
203    @Override
204    public boolean equals(final Object other) {
205        if (!(other instanceof SubjectType)) {
206            return false;
207        }
208
209        final SubjectType<?> that = (SubjectType<?>) other;
210        return this.name.equals(that.name)
211                && this.identifierType.equals(that.identifierType);
212    }
213
214    @Override
215    public String toString() {
216        return "SubjectType<" + this.identifierType.getSimpleName() + ">(name=" + this.name + ")";
217    }
218
219    /**
220     * A builder for a subject type
221     * @param <I> identifier type
222     */
223    public static final class Builder<I> {
224        private final String name;
225        private final Class<I> identifierType;
226        private boolean transientHasPriority = true;
227        private Function<I, Boolean> undefinedValueProvider = $ -> false;
228        private Function<I, @Nullable ?> associatedObjectProvider = $ -> null;
229        private Function<String, @Nullable I> friendlyDeserializer = $ -> null;
230        private @Nullable Function<String, I> identifierDeserializer; // required
231        private @Nullable Function<I, String> identifierSerializer; // required
232
233        Builder(final String name, final Class<I> identifierType) {
234            requireNonNull(name, "name");
235            requireNonNull(identifierType, "identifierType");
236            this.name = name;
237            this.identifierType = identifierType;
238        }
239
240        Builder(final SubjectType<I> existing) {
241            this.name = existing.name;
242            this.identifierType = existing.identifierType;
243            this.transientHasPriority = existing.transientHasPriority;
244            this.undefinedValueProvider = existing.undefinedValueProvider;
245            this.associatedObjectProvider = existing.associatedObjectProvider;
246            this.friendlyDeserializer = existing.friendlyDeserializer;
247            this.identifierDeserializer = existing.identifierDeserializer;
248            this.identifierSerializer = existing.identifierSerializer;
249        }
250
251        /**
252         * Whether or not this subject resolves data transient-first or persistent first.
253         *
254         * <p>The default value for this property is {@code true}.</p>
255         *
256         * @param priority if transient data should take priority over persistent.
257         * @return this builder
258         * @since 2.0.0
259         */
260        public Builder<I> transientHasPriority(final boolean priority) {
261            this.transientHasPriority = priority;
262            return this;
263        }
264
265        /**
266         * Set the provider for a fallback permissions value when an undefined
267         * value ({@code 0}) is resolved.
268         *
269         * @param provider the value provider
270         * @return this builder
271         * @since 2.0.0
272         */
273        public Builder<I> undefinedValues(final Function<I, Boolean> provider) {
274            requireNonNull(provider, "provider");
275            this.undefinedValueProvider = provider;
276            return this;
277        }
278
279        public Builder<I> serializedBy(final Function<I, String> serializer) {
280            this.identifierSerializer = requireNonNull(serializer, "serializer");
281            return this;
282        }
283
284        /**
285         * Attempt to deserialize an identifier from the raw input
286         *
287         * <p>On failure, the function may throw a {@link InvalidIdentifierException}</p>
288         *
289         * @param deserializer the deserialization function
290         * @return this builder
291         */
292        public Builder<I> deserializedBy(final Function<String, I> deserializer) {
293            this.identifierDeserializer = requireNonNull(deserializer, "deserializer");
294            return this;
295        }
296
297        /**
298         * Provide a function that can resolve a deserialized identifier from a 'friendly' name.
299         *
300         * <p>This function should never throw an {@link InvalidIdentifierException}.</p>
301         *
302         * @param coercer the coercion function
303         * @return this builder
304         */
305        public Builder<I> friendlyNameResolvedBy(final Function<String, @Nullable I> coercer) {
306            this.friendlyDeserializer = requireNonNull(coercer, "coercer");
307            return this;
308        }
309
310        /**
311         * Set a provider for associated objects.
312         *
313         * @param provider the associated object provider. may return null if no associated object is available.
314         * @return this builder
315         * @since 2.0.0
316         */
317        public Builder<I> associatedObjects(final Function<I, @Nullable ?> provider) {
318            this.associatedObjectProvider = provider;
319            return this;
320        }
321
322        /**
323         * Create a subject type with a fixed set of entries.
324         *
325         * @param entries a map of identifier to associated object provider
326         * @return this builder
327         * @since 2.0.0
328         */
329        public Builder<I> fixedEntries(final Map<I, ? extends Supplier<@Nullable ?>> entries) {
330            requireNonNull(entries, "entries");
331
332            // Use the map for discovering associated objects
333            this.associatedObjectProvider = ident -> {
334                final Supplier<?> value = entries.get(ident);
335                return value == null ? null : value.get();
336            };
337
338            // And restrict the range of our identifier deserializer to available entries.
339            final @Nullable Function<String, I> oldDeserializer = this.identifierDeserializer;
340            if (oldDeserializer == null) {
341                throw new IllegalStateException("An identifier deserializer must have already been set "
342                        + "to be able to restrict the valid identifiers.");
343            }
344            this.identifierDeserializer = serialized -> {
345                final I candidate = oldDeserializer.apply(serialized);
346                if (!entries.containsKey(candidate)) {
347                    throw new InvalidIdentifierException(serialized);
348                }
349                return candidate;
350            };
351            return this;
352        }
353
354        /**
355         * Create a subject type with a fixed set of entries.
356         *
357         * @param entries a map of identifier to associated object provider
358         * @return this builder
359         * @since 2.0.0
360         */
361        @SafeVarargs
362        public final Builder<I> fixedEntries(final Map.Entry<I, ? extends Supplier<@Nullable ?>>... entries) {
363            PMap<I, Supplier<@Nullable ?>> result = HashTreePMap.empty();
364            for (final Map.Entry<I, ? extends Supplier<@Nullable ?>> entry : entries) {
365                result = result.plus(entry.getKey(), entry.getValue());
366            }
367            return this.fixedEntries(result);
368        }
369
370        /**
371         * Create a subject type from the provided parameters
372         *
373         * @return the parameters
374         */
375        public SubjectType<I> build() {
376            return new SubjectType<>(this);
377        }
378
379    }
380}