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