    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  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 org.apache.commons.collections4.keyvalue;
    018
    019import java.io.Serializable;
    020import java.lang.reflect.Array;
    021import java.util.Arrays;
    022import java.util.Objects;
    023
    024/**
    025 * A {@code MultiKey} allows multiple map keys to be merged together.
    026 * <p>
    027 * The purpose of this class is to avoid the need to write code to handle
    028 * maps of maps. An example might be the need to look up a file name by
    029 * key and locale. The typical solution might be nested maps. This class
    030 * can be used instead by creating an instance passing in the key and locale.
    031 * </p>
    032 * <p>
    033 * Example usage:
    034 * </p>
    035 * <pre>
    036 * // populate map with data mapping key+locale to localizedText
    037 * Map map = new HashMap();
    038 * MultiKey multiKey = new MultiKey(key, locale);
    039 * map.put(multiKey, localizedText);
    040 *
    041 * // later retrieve the localized text
    042 * MultiKey multiKey = new MultiKey(key, locale);
    043 * String localizedText = (String) map.get(multiKey);
    044 * </pre>
    045 *
    046 * @param <K> the type of keys
    047 * @since 3.0
    048 */
    049public class MultiKey<K> implements Serializable {
    050    // This class could implement List, but that would confuse its purpose
    051
    052    /** Serialisation version */
    053    private static final long serialVersionUID = 4465448607415788805L;
    054
    055    @SuppressWarnings("unchecked")
    056    private static <T> Class<? extends T> getClass(final T value) {
    057        return (Class<? extends T>) (value == null ? Object.class : value.getClass());
    058    }
    059
    060    private static <T> Class<? extends T> getComponentType(final T... values) {
    061        @SuppressWarnings("unchecked")
    062        final Class<? extends T> rootClass = (Class<? extends T>) Object.class;
    063        if (values == null) {
    064            return rootClass;
    065        }
    066        Class<? extends T> prevClass = values.length > 0 ? getClass(values[0]) : rootClass;
    067        for (int i = 1; i < values.length; i++) {
    068            final Class<? extends T> classI = getClass(values[i]);
    069            if (prevClass != classI) {
    070                return rootClass;
    071            }
    072            prevClass = classI;
    073        }
    074        return prevClass;
    075    }
    076
    077    private static <T> T[] newArray(final T key1, final T key2) {
    078        @SuppressWarnings("unchecked")
    079        final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2), 2);
    080        array[0] = key1;
    081        array[1] = key2;
    082        return array;
    083    }
    084
    085    private static <T> T[] newArray(final T key1, final T key2, final T key3) {
    086        @SuppressWarnings("unchecked")
    087        final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3), 3);
    088        array[0] = key1;
    089        array[1] = key2;
    090        array[2] = key3;
    091        return array;
    092    }
    093
    094    private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4) {
    095        @SuppressWarnings("unchecked")
    096        final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4), 4);
    097        array[0] = key1;
    098        array[1] = key2;
    099        array[2] = key3;
    100        array[3] = key4;
    101        return array;
    102    }
    103
    104    private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4, final T key5) {
    105        @SuppressWarnings("unchecked")
    106        final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4, key5), 5);
    107        array[0] = key1;
    108        array[1] = key2;
    109        array[2] = key3;
    110        array[3] = key4;
    111        array[4] = key5;
    112        return array;
    113    }
    114
    115    /** The individual keys */
    116    private final K[] keys;
    117
    118    /** The cached hashCode */
    119    private transient int hashCode;
    120
    121    /**
    122     * Constructor taking two keys.
    123     * <p>
    124     * The keys should be immutable
    125     * If they are not then they must not be changed after adding to the MultiKey.
    126     *
    127     * @param key1  the first key
    128     * @param key2  the second key
    129     */
    130    public MultiKey(final K key1, final K key2) {
    131        this(newArray(key1, key2), false);
    132    }
    133
    134    /**
    135     * Constructor taking three keys.
    136     * <p>
    137     * The keys should be immutable
    138     * If they are not then they must not be changed after adding to the MultiKey.
    139     *
    140     * @param key1  the first key
    141     * @param key2  the second key
    142     * @param key3  the third key
    143     */
    144    public MultiKey(final K key1, final K key2, final K key3) {
    145        this(newArray(key1, key2, key3), false);
    146    }
    147
    148    /**
    149     * Constructor taking four keys.
    150     * <p>
    151     * The keys should be immutable
    152     * If they are not then they must not be changed after adding to the MultiKey.
    153     *
    154     * @param key1  the first key
    155     * @param key2  the second key
    156     * @param key3  the third key
    157     * @param key4  the fourth key
    158     */
    159    public MultiKey(final K key1, final K key2, final K key3, final K key4) {
    160        this(newArray(key1, key2, key3, key4), false);
    161    }
    162
    163    /**
    164     * Constructor taking five keys.
    165     * <p>
    166     * The keys should be immutable
    167     * If they are not then they must not be changed after adding to the MultiKey.
    168     *
    169     * @param key1  the first key
    170     * @param key2  the second key
    171     * @param key3  the third key
    172     * @param key4  the fourth key
    173     * @param key5  the fifth key
    174     */
    175    public MultiKey(final K key1, final K key2, final K key3, final K key4, final K key5) {
    176        this(newArray(key1, key2, key3, key4, key5), false);
    177    }
    178
    179    /**
    180     * Constructor taking an array of keys which is cloned.
    181     * <p>
    182     * The keys should be immutable
    183     * If they are not then they must not be changed after adding to the MultiKey.
    184     * <p>
    185     * This is equivalent to {@code new MultiKey(keys, true)}.
    186     *
    187     * @param keys  the array of keys, not null
    188     * @throws NullPointerException if the key array is null
    189     */
    190    public MultiKey(final K[] keys) {
    191        this(keys, true);
    192    }
    193
    194    /**
    195     * Constructor taking an array of keys, optionally choosing whether to clone.
    196     * <p>
    197     * <b>If the array is not cloned, then it must not be modified.</b>
    198     * <p>
    199     * This method is public for performance reasons only, to avoid a clone.
    200     * The hash code is calculated once here in this method.
    201     * Therefore, changing the array passed in would not change the hash code but
    202     * would change the equals method, which is a bug.
    203     * <p>
    204     * This is the only fully safe usage of this constructor, as the object array
    205     * is never made available in a variable:
    206     * <pre>
    207     * new MultiKey(new Object[] {...}, false);
    208     * </pre>
    209     * <p>
    210     * The keys should be immutable
    211     * If they are not then they must not be changed after adding to the MultiKey.
    212     *
    213     * @param keys  the array of keys, not null
    214     * @param makeClone  true to clone the array, false to assign it
    215     * @throws NullPointerException if the key array is null
    216     * @since 3.1
    217     */
    218    public MultiKey(final K[] keys, final boolean makeClone) {
    219        Objects.requireNonNull(keys, "keys");
    220        this.keys = makeClone ? keys.clone() : keys;
    221        calculateHashCode(keys);
    222    }
    223
    224    /**
    225     * Calculate the hash code of the instance using the provided keys.
    226     * @param keys the keys to calculate the hash code for
    227     */
    228    private void calculateHashCode(final Object[] keys) {
    229        int total = 0;
    230        for (final Object key : keys) {
    231            if (key != null) {
    232                total ^= key.hashCode();
    233            }
    234        }
    235        hashCode = total;
    236    }
    237
    238    /**
    239     * Compares this object to another.
    240     * <p>
    241     * To be equal, the other object must be a {@code MultiKey} with the
    242     * same number of keys which are also equal.
    243     *
    244     * @param other  the other object to compare to
    245     * @return true if equal
    246     */
    247    @Override
    248    public boolean equals(final Object other) {
    249        if (other == this) {
    250            return true;
    251        }
    252        if (other instanceof MultiKey) {
    253            final MultiKey<?> otherMulti = (MultiKey<?>) other;
    254            return Arrays.equals(keys, otherMulti.keys);
    255        }
    256        return false;
    257    }
    258
    259    /**
    260     * Gets the key at the specified index.
    261     * <p>
    262     * The key should be immutable.
    263     * If it is not then it must not be changed.
    264     *
    265     * @param index  the index to retrieve
    266     * @return the key at the index
    267     * @throws IndexOutOfBoundsException if the index is invalid
    268     * @since 3.1
    269     */
    270    public K getKey(final int index) {
    271        return keys[index];
    272    }
    273
    274    /**
    275     * Gets a clone of the array of keys.
    276     * <p>
    277     * The keys should be immutable
    278     * If they are not then they must not be changed.
    279     *
    280     * @return the individual keys
    281     */
    282    public K[] getKeys() {
    283        return keys.clone();
    284    }
    285
    286    /**
    287     * Gets the combined hash code that is computed from all the keys.
    288     * <p>
    289     * This value is computed once and then cached, so elements should not
    290     * change their hash codes once created (note that this is the same
    291     * constraint that would be used if the individual keys elements were
    292     * themselves {@link java.util.Map Map} keys).
    293     *
    294     * @return the hash code
    295     */
    296    @Override
    297    public int hashCode() {
    298        return hashCode;
    299    }
    300
    301    /**
    302     * Recalculate the hash code after deserialization. The hash code of some
    303     * keys might have change (hash codes based on the system hash code are
    304     * only stable for the same process).
    305     * @return the instance with recalculated hash code
    306     */
    307    protected Object readResolve() {
    308        calculateHashCode(keys);
    309        return this;
    310    }
    311
    312    /**
    313     * Gets the size of the list of keys.
    314     *
    315     * @return the size of the list of keys
    316     * @since 3.1
    317     */
    318    public int size() {
    319        return keys.length;
    320    }
    321
    322    /**
    323     * Gets a debugging string version of the key.
    324     *
    325     * @return a debugging string
    326     */
    327    @Override
    328    public String toString() {
    329        return "MultiKey" + Arrays.toString(keys);
    330    }
    331}