/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 net.formio.common.heterog; import java.io.Serializable; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; /** * Default implementation of typesafe heterogeneous container (heterogeneous mapping). * <p> * Immutable: No, items can be added/removed to/from internal state. * * @author Radek Beran */ class DefaultHeterogMap<K> implements Serializable, HeterogMap<K>, Map<TypedKey<K, ?>, Object> { private static final long serialVersionUID = 1584641968968664353L; // Unbounded wildcard "?" is related to key of map, not a map itself // Java's type system is NOT enough powerful to express relationship // between type in TypedKey and type of value, but that relationship // is properly handled in putTyped and getTyped methods. private final Map<TypedKey<K, ?>, Object> container; DefaultHeterogMap() { this(new HashMap<TypedKey<K, ?>, Object>()); } DefaultHeterogMap(int initialCapacity) { this(new HashMap<TypedKey<K, ?>, Object>(initialCapacity)); } DefaultHeterogMap(int initialCapacity, float loadFactor) { this(new HashMap<TypedKey<K, ?>, Object>(initialCapacity, loadFactor)); } /** * Constructs new heterogeneous container using given underlying map. If given map * is another heterogeneous container, it constructs shallow copy of given container. * @param bakingMap */ DefaultHeterogMap(Map<TypedKey<K, ?>, Object> bakingMap) { Map<TypedKey<K, ?>, Object> bakingMapping = bakingMap; if (bakingMapping == null) throw new IllegalArgumentException("bakingMap cannot be null"); if (bakingMapping instanceof DefaultHeterogMap) { DefaultHeterogMap<K> srcContainer = (DefaultHeterogMap<K>)bakingMapping; bakingMapping = copyUnderlyingMap(srcContainer.container); } this.container = bakingMapping; } // ---- HeterogMap: @Override public <T> T putTyped(TypedKey<K, T> key, T value) { if (key == null) throw new NullPointerException("key cannot be null"); T prev = getTyped(key); putTypedInternal(key, value); return prev; } @SuppressWarnings("unchecked") // we know heterogeneous container can contain only correctly paired keys and values @Override public void putAllFromSource(HeterogMap<K> c) { if (c != null) { for (TypedKey<K, ?> id : c.keySet()) { putTyped((TypedKey<K, Object>)id, c.getTyped(id)); } } } @Override public <T> T getTyped(TypedKey<K, T> key) { if (key == null) throw new NullPointerException("key cannot be null"); // Dynamic check that value is of type represented by valueClass in key // It returns correct type. If type is incorrect, ClassCastException is thrown. return key.getValueClass().cast(container.get(key)); } @Override public <T> T removeTyped(TypedKey<K, T> key) { T value = getTyped(key); // value can be null for the key, but still present this.container.remove(key); return value; } @Override public <T> boolean containsKey(TypedKey<K, T> key) { return this.container.containsKey(key); } // ---- @Override public int size() { return container.size(); } @Override public boolean isEmpty() { return this.container.isEmpty(); } @SuppressWarnings("unchecked") // we know only TypedKey key can be put into the map @Override public Object remove(Object key) { if (!(key instanceof TypedKey)) throw new IllegalArgumentException("key must be of type TypedKey"); return removeTyped((TypedKey<K, Object>)key); } @Override public void clear() { this.container.clear(); } @Override public boolean containsKey(Object key) { return this.container.containsKey(key); } @Override public boolean containsValue(Object value) { return this.container.containsValue(value); } @Override public Object get(Object key) { return this.container.get(key); } @SuppressWarnings("unchecked") // type of value is checked inside the internally used putTyped method @Override public Object put(TypedKey<K, ?> key, Object value) { return putTyped((TypedKey<K, Object>)key, value); } @Override public void putAll(Map<? extends TypedKey<K, ?>, ? extends Object> m) { for (Map.Entry<? extends TypedKey<K, ?>, Object> e : this.container.entrySet()) put(e.getKey(), e.getValue()); } @Override public Set<TypedKey<K, ?>> keySet() { return this.container.keySet(); } @Override public Collection<Object> values() { return this.container.values(); } @Override public Set<Map.Entry<TypedKey<K, ?>, Object>> entrySet() { return this.container.entrySet(); } /** * Compares the specified object with this map for equality. Returns * <tt>true</tt> if the given object is also a map and the two maps * represent the same mappings. More formally, two maps <tt>m1</tt> and * <tt>m2</tt> represent the same mappings if * <tt>m1.entrySet().equals(m2.entrySet())</tt>. This ensures that the * <tt>equals</tt> method works properly across different implementations * of the <tt>Map</tt> interface. * * <p>This implementation first checks if the specified object is this map; * if so it returns <tt>true</tt>. Then, it checks if the specified * object is a map whose size is identical to the size of this map; if * not, it returns <tt>false</tt>. If so, it iterates over this map's * <tt>entrySet</tt> collection, and checks that the specified map * contains each mapping that this map contains. If the specified map * fails to contain such a mapping, <tt>false</tt> is returned. If the * iteration completes, <tt>true</tt> is returned. * * @param o object to be compared for equality with this map * @return <tt>true</tt> if the specified object is equal to this map */ @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof HeterogMap)) return false; HeterogMap<K> m = (HeterogMap<K>)o; if (m.size() != size()) return false; try { Iterator<Entry<TypedKey<K, ?>, Object>> i = entrySet().iterator(); while (i.hasNext()) { Entry<TypedKey<K, ?>, Object> e = i.next(); TypedKey<K, ?> key = e.getKey(); Object value = e.getValue(); if (value == null) { if (m.getTyped(key) != null || !m.containsKey(key)) return false; } else { if (!value.equals(m.getTyped(key))) return false; } } } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; } return true; } /** * Returns the hash code value for this map. The hash code of a map is * defined to be the sum of the hash codes of each entry in the map's * <tt>entrySet()</tt> view. This ensures that <tt>m1.equals(m2)</tt> * implies that <tt>m1.hashCode()==m2.hashCode()</tt> for any two maps * <tt>m1</tt> and <tt>m2</tt>, as required by the general contract of * {@link Object#hashCode}. * * <p>This implementation iterates over <tt>entrySet()</tt>, calling * {@link Map.Entry#hashCode hashCode()} on each element (entry) in the * set, and adding up the results. * * @return the hash code value for this map * @see Map.Entry#hashCode() * @see Object#equals(Object) * @see Set#equals(Object) */ @Override public int hashCode() { int h = 0; Iterator<Entry<TypedKey<K, ?>, Object>> i = entrySet().iterator(); while (i.hasNext()) h += i.next().hashCode(); return h; } @Override public Map<K, Object> asMap() { Map<K, Object> map = new HashMap<K, Object>(); for (Map.Entry<TypedKey<K, ?>, Object> e : entrySet()) { map.put(e.getKey().getKey(), e.getValue()); } return map; } /** * Returns a string representation of this map. The string representation * consists of a list of key-value mappings in the order returned by the * map's <tt>entrySet</tt> view's iterator, enclosed in braces * (<tt>"{}"</tt>). Adjacent mappings are separated by the characters * <tt>", "</tt> (comma and space). Each key-value mapping is rendered as * the key followed by an equals sign (<tt>"="</tt>) followed by the * associated value. Keys and values are converted to strings as by * {@link String#valueOf(Object)}. * * @return a string representation of this map */ @Override public String toString() { Iterator<Entry<TypedKey<K, ?>, Object>> i = entrySet().iterator(); if (! i.hasNext()) return "{}"; StringBuilder sb = new StringBuilder(); sb.append('{'); for (;;) { Entry<TypedKey<K, ?>, Object> e = i.next(); TypedKey<K, ?> key = e.getKey(); Object value = e.getValue(); sb.append(key.toString()); sb.append('='); sb.append(value == this ? "(this Map)" : value); if (! i.hasNext()) return sb.append('}').toString(); sb.append(',').append(' '); } } private <T> void putTypedInternal(TypedKey<K, T> key, T value) { // Cast prevents from invalid type when raw method without generics is used this.container.put(key, key.getValueClass().cast(value)); } private static <K> Map<TypedKey<K, ?>, Object> copyUnderlyingMap(Map<TypedKey<K, ?>, Object> source) { if (source == null) throw new IllegalArgumentException("source cannot be null"); if (source instanceof LinkedHashMap) { return new LinkedHashMap<TypedKey<K, ?>, Object>(source); } return new HashMap<TypedKey<K, ?>, Object>(source); } }