/* * JBoss, Home of Professional Open Source. * Copyright 2015 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.wildfly.security.authz; import java.util.AbstractCollection; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; import org.wildfly.common.Assert; /** * <p>A collection of string attributes. * * <p>By default, this interface provides a default implementation for all methods that perform writes to the collection. The default * implementation will always throw a {@link UnsupportedOperationException}, which means the collection is <em>read-only</em> by default. * * <p>If an implementation needs to also support writes it must override and implement all these default methods. * * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a> */ public interface Attributes { /** * Empty, read-only attribute collection. */ Attributes EMPTY = new Attributes() { @Override public Collection<Entry> entries() { return Collections.emptySet(); } @Override public int size(final String key) { return 0; } @Override public boolean remove(final String key) { return false; } @Override public String get(final String key, final int idx) { return null; } @Override public Entry get(final String key) { return new SimpleAttributesEntry(this, key); } @Override public int size() { return 0; } }; /** * Get the entry collection. Changes to the entry collection will modify this attribute collection, if it is * writable. The returned entries will remain up to date with the state of this collection. * * @return the entry collection */ Collection<Entry> entries(); /** * Get the number of values mapped to the given key. * * @param key the key * @return the number of mapped values */ int size(String key); /** * Get the collection of values for the given key. The result may implement {@link SetEntry} if the values * are distinct (for example, a role or group set). * * @param key the attribute name * @return the (possibly empty) attribute value collection */ Entry get(String key); /** * Get the mapping for the given key at the given position. * * @param key the key * @param idx the index * @return the mapping value * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or greater than or equal to {@code size(key)} */ String get(String key, int idx); /** * Get the number of keys in this attribute collection. * * @return the number of keys */ int size(); /** * Remove all values for the given key from this collection. * * @param key the key * @return {@code true} if the key was found, {@code false} otherwise * @throws UnsupportedOperationException if this method is not implemented and the operation is not supported */ default boolean remove(String key) { throw Assert.unsupported(); } /** * Add a mapping for the given key at the given position. * * @param key the key * @param idx the index * @param value the mapping value * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or greater than or equal to {@code size(key)} * @throws UnsupportedOperationException if this method is not implemented and the operation is not supported */ default void add(String key, int idx, String value) { throw Assert.unsupported(); } /** * Modify the mapping for the given key at the given position. * * @param key the key * @param idx the index * @param value the mapping value * @return the previous mapping value * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or greater than or equal to {@code size(key)} * @throws UnsupportedOperationException if this method is not implemented and the operation is not supported */ default String set(String key, int idx, String value) { throw Assert.unsupported(); } /** * Remove and return the mapping for the given key at the given position. All later entries for that key are shifted * up to fill in the gap left by the removed element. * * @param key the key * @param idx the index * @return the previous mapping value * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or greater than or equal to {@code size(key)} * @throws UnsupportedOperationException if this method is not implemented and the operation is not supported */ default String remove(String key, int idx) { throw Assert.unsupported(); } /** * Clear this collection, resetting its size to zero. * * @throws UnsupportedOperationException if this method is not implemented and the operation is not supported */ default void clear() { throw Assert.unsupported(); } /** * Remove all values for the given key from this collection, copying the values into a list which is returned. * * @param key the key * @return the values as a list (not {@code null}) */ default List<String> copyAndRemove(String key) { final Entry values = get(key); List<String> copy = values.isEmpty() ? Collections.emptyList() : new ArrayList<>(values); remove(key); return copy; } /** * Get all the values of all the keys in this collection. The returned collection can be used to modify this * attributes collection. * * @return the collection of all values (not {@code null}) */ default Collection<String> values() { return new AbstractCollection<String>() { public Iterator<String> iterator() { final Iterator<Entry> entries = entries().iterator(); return new Iterator<String>() { private Iterator<String> values; public boolean hasNext() { for (;;) { if (values == null) { if (! entries.hasNext()) { return false; } values = entries.next().iterator(); } else if (values.hasNext()) { return true; } else { values = null; } } } public String next() { if (! hasNext()) throw new NoSuchElementException(); return values.next(); } public void remove() { final Iterator<String> values = this.values; if (values == null) { throw new IllegalStateException(); } values.remove(); } }; } public void clear() { Attributes.this.clear(); } public boolean removeAll(final Collection<?> c) { boolean changed = false; for (Entry entries : entries()) { changed = entries.removeAll(c) || changed; } return changed; } public boolean retainAll(final Collection<?> c) { boolean changed = false; for (Entry entries : entries()) { changed = entries.retainAll(c) || changed; } return changed; } public boolean isEmpty() { for (Entry entries : entries()) { if (! entries.isEmpty()) { return false; } } return true; } public int size() { int size = 0; for (Entry entries : entries()) { size += entries.size(); } return size; } }; } /** * Get a set comprised of all the keys in this collection. The returned set can be used to modify this attributes * collection. * * @return the set of all keys (not {@code null}) */ default Set<String> keySet() { return new AbstractSet<String>() { public Iterator<String> iterator() { final Iterator<Entry> entries = entries().iterator(); return new Iterator<String>() { public boolean hasNext() { return entries.hasNext(); } public String next() { return entries.next().getKey(); } public void remove() { entries.remove(); } }; } public boolean contains(final Object o) { return o instanceof String && Attributes.this.containsKey((String) o); } public boolean remove(final Object o) { return o instanceof String && Attributes.this.remove((String) o); } public void clear() { Attributes.this.clear(); } public int size() { return Attributes.this.size(); } }; } /** * Conditionally set a specific value of a given key to a new value, if the existing value matches the {@code expect} * parameter. * * @param key the key * @param idx the index * @param expect the expected value * @param update the value to set * @return {@code true} if the actual value matched the expected value and was updated, {@code false} otherwise * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or greater than or equal to {@code size(key)} */ default boolean set(String key, int idx, String expect, String update) { Assert.checkNotNullParam("update", update); if (expect == null || idx < 0 || idx >= size(key) || ! get(key, idx).equals(expect)) { return false; } set(key, idx, update); return true; } /** * Get the index of the first occurrence of the given value at the given key, if any. * * @param key the key * @param value the value * @return the index, or -1 if the value was not found at the given key */ default int indexOf(String key, String value) { final int size = size(key); for (int i = 0; i < size; i ++) { if (get(key, i).equals(value)) { return i; } } return -1; } /** * Get the index of the last occurrence of the given value at the given key, if any. * * @param key the key * @param value the value * @return the index, or -1 if the value was not found at the given key */ default int lastIndexOf(String key, String value) { final int size = size(key); for (int i = size - 1; i >= 0; i --) { if (get(key, i).equals(value)) { return i; } } return -1; } /** * Remove all the values for the given key between the {@code from} index (inclusive) and the {@code to} index * (exclusive). * * @param key the key * @param from the start index (inclusive) * @param to the end index (exclusive) * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or greater than or equal to {@code size(key)} */ default void removeRange(String key, int from, int to) { for (int i = to - 1; i >= from; i --) { remove(key, i); } } /** * Get the first value mapped to the given key. * * @param key the key * @return the value * @throws IndexOutOfBoundsException if there are no values for the given key */ default String getFirst(String key) { return get(key, 0); } /** * Get the last value mapped to the given key. * * @param key the key * @return the value * @throws IndexOutOfBoundsException if there are no values for the given key */ default String getLast(String key) { return get(key, size(key) - 1); } /** * Add a value before the first mapping for the given key. * * @param key the key * @param value the value */ default void addFirst(String key, String value) { add(key, 0, value); } /** * Add a value after the last mapping for the given key. * * @param key the key * @param value the value */ default void addLast(String key, String value) { add(key, size(key), value); } /** * Remove the first value mapped to the given key. * * @param key the key * @return the value * @throws IndexOutOfBoundsException if there are no values for the given key */ default String removeFirst(String key) { return remove(key, 0); } /** * Remove the last value mapped to the given key. * * @param key the key * @return the value * @throws IndexOutOfBoundsException if there are no values for the given key */ default String removeLast(String key) { return remove(key, size(key) - 1); } /** * Remove the mapping for the given key at the given position if it matches the given existing value. All later * entries for that key are shifted up to fill in the gap left by the removed element. * * @param key the key * @param idx the index * @param value the expected previous mapping value * @return {@code true} if the value matched and was removed, {@code false} otherwise * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or greater than or equal to {@code size(key)} */ default boolean remove(String key, int idx, String value) { if (get(key, idx).equals(value)) { remove(key, idx); return true; } else { return false; } } /** * Remove the first occurrence of the given value under the given key, if any. * * @param key the key * @param value the value to remove * @return {@code true} if the value was found and removed, {@code false} otherwise */ default boolean removeFirst(String key, String value) { final int idx = indexOf(key, value); return idx >= 0 && remove(key, idx, value); } /** * Remove the last occurrence of the given value under the given key, if any. * * @param key the key * @param value the value to remove * @return {@code true} if the value was found and removed, {@code false} otherwise */ default boolean removeLast(String key, String value) { final int idx = lastIndexOf(key, value); return idx >= 0 && remove(key, idx, value); } /** * Remove the all occurrences of the given value under the given key, if any. * * @param key the key * @param value the value to remove * @return {@code true} if the value was found and removed, {@code false} otherwise */ default boolean removeAll(String key, String value) { int idx = lastIndexOf(key, value); if (idx == -1) return false; while (idx >= 0) { remove(key, idx, value); idx = lastIndexOf(key, value); } return true; } /** * Add all the values from the given map to this attributes collection. * * @param map the map to copy from * @return {@code true} if elements were added, {@code false} otherwise */ default boolean addAll(Map<String, ? extends Collection<String>> map) { Assert.checkNotNullParam("map", map); boolean changed = false; for (Map.Entry<String, ? extends Collection<String>> entry : map.entrySet()) { final Collection<String> value = entry.getValue(); if (value != null && ! value.isEmpty()) { final String key = entry.getKey(); changed = addAll(key, value) || changed; } } return changed; } /** * Add all the values from the given collection to the value collection for the given key. * * @param key the key * @param values the values to add * @return {@code true} if elements were added, {@code false} otherwise */ default boolean addAll(String key, Collection<String> values) { Assert.checkNotNullParam("key", key); Assert.checkNotNullParam("values", values); boolean changed = false; for (String s : values) { if (s != null) { addLast(key, s); changed = true; } } return changed; } /** * Determine if the given key has values in this collection. * * @param key the key * @return {@code true} if the key has values, {@code false} otherwise */ default boolean containsKey(String key) { return key != null && size(key) > 0; } /** * Determine if the given key has a mapping for the given value in this collection. * * @param key the key * @param value the value * @return {@code true} if the key has a mapping to the given value, {@code false} otherwise */ default boolean containsValue(String key, String value) { return key != null && value != null && indexOf(key, value) >= 0; } /** * Replace the mapping for the given key with the values copied from the given collection. * * @param key the key * @param values the new values * @return a list containing the previously mapped values */ default List<String> copyAndReplace(String key, Collection<String> values) { final List<String> old = copyAndRemove(key); addAll(key, values); return old; } /** * Determine if this collection is empty. * * @return {@code true} if the collection is empty, {@code false} otherwise */ default boolean isEmpty() { return size() == 0; } /** * Returns a read-only instance of this instance. * * @return a read-only instance of this instance. */ default Attributes asReadOnly() { return new Attributes() { private Map<String, Entry> entryCache = new HashMap<>(); private Collection<Entry> cachedEntries; @Override public Collection<Entry> entries() { if (this.cachedEntries != null) { return this.cachedEntries; } return this.cachedEntries = new AbstractCollection<Entry>() { @Override public Iterator<Entry> iterator() { Iterator<String> iterator = Attributes.this.keySet().iterator(); return new Iterator<Entry>() { @Override public boolean hasNext() { return iterator.hasNext(); } @Override public Entry next() { return get(iterator.next()); } }; } @Override public int size() { return Attributes.this.keySet().size(); } }; } @Override public int size(String key) { return Attributes.this.size(key); } @Override public String get(String key, int idx) { return Attributes.this.get(key, idx); } @Override public int size() { return Attributes.this.size(); } @Override public Entry get(String key) { return this.entryCache.computeIfAbsent(key, s -> new SimpleAttributesEntry(this, s)); } }; } /** * The entry collection for a mapping. */ interface Entry extends List<String> { /** * Get the mapping key. * * @return the mapping key */ String getKey(); /** * Remove all the values for the given key between the {@code from} index (inclusive) and the {@code to} index * (exclusive). * * @param from the start index (inclusive) * @param to the end index (exclusive) * @throws IndexOutOfBoundsException if {@code from} or {@code to} is outside of the allowed range */ void removeRange(int from, int to); /** * Create a spliterator over the elements of this ordered and non-null collection. * * @return the spliterator */ default Spliterator<String> spliterator() { return Spliterators.spliterator(this, Spliterator.ORDERED | Spliterator.NONNULL); } } /** * The entry collection for a mapping whose values are a distinct set. */ interface SetEntry extends Entry, Set<String> { /** * Create a spliterator over the elements of this distinct, ordered, and non-null collection. * * @return the spliterator */ default Spliterator<String> spliterator() { return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.NONNULL); } } }