All Downloads are FREE. Search and download functionalities are using the official Maven repository.

aQute.lib.collections.MultiMap Maven / Gradle / Ivy

package aQute.lib.collections;

import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Formatter;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;

public class MultiMap implements Map> {
	private final boolean			noduplicates;
	private final Class			keyClass;
	private final Class			valueClass;
	private final Map>	map;

	public MultiMap() {
		this(false);
	}

	@SuppressWarnings("unchecked")
	public MultiMap(boolean noduplicates) {
		this((Class) Object.class, (Class) Object.class, noduplicates);
	}

	@SuppressWarnings("unchecked")
	public MultiMap(Class keyClass, Class valueClass, boolean noduplicates) {
		this.noduplicates = noduplicates;
		this.keyClass = (Class) keyClass;
		this.valueClass = (Class) valueClass;
		this.map = new LinkedHashMap<>();
	}

	public MultiMap(Map> other) {
		this();
		addAll(other);
	}

	public  MultiMap(MultiMap other) {
		this(other.keyClass, other.valueClass, other.noduplicates);
		addAll(other);
	}

	private List newValue(K key) {
		if (valueClass != Object.class) {
			return Collections.checkedList(new ArrayList<>(), valueClass);
		}
		return new ArrayList<>();
	}

	public boolean add(K key, V value) {
		assert keyClass.isInstance(key);
		assert valueClass.isInstance(value);
		List values = computeIfAbsent(key, this::newValue);
		if (noduplicates && values.contains(value)) {
			return false;
		}
		return values.add(value);
	}

	public boolean addAll(K key, Collection value) {
		assert keyClass.isInstance(key);
		if ((value == null) || value.isEmpty()) {
			return false;
		}
		List values = computeIfAbsent(key, this::newValue);
		if (noduplicates) {
			boolean added = false;
			for (V v : value) {
				assert valueClass.isInstance(v);
				if (!values.contains(v)) {
					values.add(v);
					added = true;
				}
			}
			return added;
		}
		return values.addAll(value);
	}

	public boolean addAll(Map> other) {
		boolean added = false;
		for (Map.Entry> entry : other.entrySet()) {
			if (addAll(entry.getKey(), entry.getValue())) {
				added = true;
			}
		}
		return added;
	}

	public boolean removeValue(K key, V value) {
		assert keyClass.isInstance(key);
		assert valueClass.isInstance(value);
		List values = get(key);
		if (values == null) {
			return false;
		}
		boolean result = values.remove(value);
		if (values.isEmpty()) {
			remove(key);
		}
		return result;
	}

	public boolean removeAll(K key, Collection value) {
		assert keyClass.isInstance(key);
		List values = get(key);
		if (values == null) {
			return false;
		}
		boolean result = values.removeAll(value);
		if (values.isEmpty()) {
			remove(key);
		}
		return result;
	}

	public Iterator iterate(K key) {
		assert keyClass.isInstance(key);
		List values = get(key);
		if (values == null) {
			return Collections.emptyIterator();
		}
		return values.iterator();
	}

	private Stream valuesStream() {
		return values().stream()
			.filter(Objects::nonNull)
			.flatMap(List::stream);
	}

	public Iterator all() {
		return valuesStream().iterator();
	}

	public Map flatten() {
		Map flattened = new LinkedHashMap<>();
		for (Map.Entry> entry : entrySet()) {
			List values = entry.getValue();
			if ((values == null) || values.isEmpty()) {
				continue;
			}
			flattened.put(entry.getKey(), values.get(0));
		}
		return flattened;
	}

	public MultiMap transpose() {
		return transpose(false);
	}

	public MultiMap transpose(boolean noduplicates) {
		MultiMap transposed = new MultiMap<>(valueClass, keyClass, noduplicates);
		for (Map.Entry> entry : entrySet()) {
			List keys = entry.getValue();
			if (keys == null) {
				continue;
			}
			K value = entry.getKey();
			for (V key : keys) {
				if (key == null) {
					continue;
				}
				transposed.add(key, value);
			}
		}
		return transposed;
	}

	/**
	 * Return a collection with all values
	 *
	 * @return all values
	 */
	public List allValues() {
		return valuesStream().collect(toList());
	}

	public static > String format(Map> map) {
		try (Formatter f = new Formatter()) {
			SortedList keys = new SortedList<>(map.keySet());
			for (Object key : keys) {
				String name = key.toString();

				SortedList values = new SortedList<>(map.get(key), null);
				String list = vertical(40, values);
				f.format("%-39s %s", name, list);
			}
			return f.toString();
		}
	}

	static String vertical(int padding, Collection used) {
		StringBuilder sb = new StringBuilder();
		String del = "";
		for (Object s : used) {
			String name = s.toString();
			sb.append(del);
			sb.append(name);
			sb.append("\r\n");
			del = pad(padding);
		}
		if (sb.length() == 0)
			sb.append("\r\n");
		return sb.toString();
	}

	static String pad(int i) {
		StringBuilder sb = new StringBuilder();
		while (i-- > 0)
			sb.append(' ');
		return sb.toString();
	}

	@Override
	public int size() {
		return map.size();
	}

	@Override
	public boolean isEmpty() {
		return map.isEmpty();
	}

	@Override
	public boolean containsKey(Object key) {
		return map.containsKey(key);
	}

	@Override
	public boolean containsValue(Object value) {
		return map.containsValue(value);
	}

	@Override
	public List get(Object key) {
		return map.get(key);
	}

	@Override
	public List put(K key, List value) {
		return map.put(key, value);
	}

	@Override
	public List remove(Object key) {
		return map.remove(key);
	}

	@Override
	public void putAll(Map> m) {
		map.putAll(m);
	}

	@Override
	public void clear() {
		map.clear();
	}

	@Override
	public Set keySet() {
		return map.keySet();
	}

	@Override
	public Collection> values() {
		return map.values();
	}

	@Override
	public Set>> entrySet() {
		return map.entrySet();
	}

	@Override
	public List getOrDefault(Object key, List defaultValue) {
		return map.getOrDefault(key, defaultValue);
	}

	@Override
	public void forEach(BiConsumer> action) {
		map.forEach(action);
	}

	@Override
	public void replaceAll(BiFunction, ? extends List> function) {
		map.replaceAll(function);
	}

	@Override
	public List putIfAbsent(K key, List value) {
		return map.putIfAbsent(key, value);
	}

	@Override
	public boolean remove(Object key, Object value) {
		return map.remove(key, value);
	}

	@Override
	public boolean replace(K key, List oldValue, List newValue) {
		return map.replace(key, oldValue, newValue);
	}

	@Override
	public List replace(K key, List value) {
		return map.replace(key, value);
	}

	@Override
	public List computeIfAbsent(K key, Function> mappingFunction) {
		return map.computeIfAbsent(key, mappingFunction);
	}

	@Override
	public List computeIfPresent(K key,
		BiFunction, ? extends List> remappingFunction) {
		return map.computeIfPresent(key, remappingFunction);
	}

	@Override
	public List compute(K key, BiFunction, ? extends List> remappingFunction) {
		return map.compute(key, remappingFunction);
	}

	@Override
	public List merge(K key, List value,
		BiFunction, ? super List, ? extends List> remappingFunction) {
		return map.merge(key, value, remappingFunction);
	}

	@Override
	public int hashCode() {
		return Objects.hash(map, keyClass, valueClass, noduplicates);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof MultiMap other)) {
			return false;
		}
		return Objects.equals(map, other.map) && Objects.equals(keyClass, other.keyClass)
			&& Objects.equals(valueClass, other.valueClass) && (noduplicates == other.noduplicates);
	}

	@Override
	public String toString() {
		return map.toString();
	}

	/**
	 * Index a collection.
	 *
	 * @param collection the collection to index
	 * @param classifier the function to map a value in the collection to the
	 *            key
	 * @return the current map
	 */
	public MultiMap groupBy(Collection collection,
		Function classifier) {
		assert collection != null;
		assert classifier != null;
		collection.forEach(v -> add(classifier.apply(v), v));
		return this;
	}

}