001    // Copyright 2004, 2005 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry.util;
016    
017    import org.apache.tapestry.Tapestry;
018    
019    import java.io.Externalizable;
020    import java.io.IOException;
021    import java.io.ObjectInput;
022    import java.io.ObjectOutput;
023    
024    /**
025     * A complex key that may be used as an alternative to nested {@link java.util.Map}s.
026     * 
027     * @author Howard Lewis Ship
028     */
029    
030    public class MultiKey implements Externalizable
031    {
032    
033        /**
034         * @since 2.0.4
035         */
036    
037        private static final long serialVersionUID = 4465448607415788806L;
038    
039        private static final int HASH_CODE_UNSET = -1;
040    
041        private transient int hashCode = HASH_CODE_UNSET;
042    
043        private Object[] _keys;
044    
045        /**
046         * Public no-arguments constructor needed to be compatible with
047         * {@link Externalizable}; this leaves the new MultiKey in a non-usable
048         * state and shouldn't be used by user code.
049         */
050    
051        public MultiKey()
052        {
053        }
054    
055        /**
056         * Builds a <code>MultiKey</code> from an array of keys. If the array is
057         * not copied, then it must not be modified.
058         * 
059         * @param keys
060         *            The components of the key.
061         * @param makeCopy
062         *            If true, a copy of the keys is created. If false, the keys are
063         *            simple retained by the <code>MultiKey</code>.
064         * @throws IllegalArgumentException
065         *             if keys is null, of if the first element of keys is null.
066         */
067    
068        public MultiKey(Object[] keys, boolean makeCopy)
069        {
070            super();
071    
072            if (keys == null || keys.length == 0)
073                throw new IllegalArgumentException(Tapestry.getMessage("MultiKey.null-keys"));
074    
075            if (keys[0] == null)
076                throw new IllegalArgumentException(Tapestry.getMessage("MultiKey.first-element-may-not-be-null"));
077    
078            if (makeCopy)
079            {
080                _keys = new Object[keys.length];
081                System.arraycopy(keys, 0, this._keys, 0, keys.length);
082            }
083            else
084                _keys = keys;
085        }
086    
087        /**
088         * Returns true if. :
089         * <ul>
090         * <li>The other object is a <code>MultiKey</code>
091         * <li>They have the same number of key elements
092         * <li>Every element is an exact match or is equal
093         * </ul>
094         */
095    
096        public boolean equals(Object other)
097        {
098            int i;
099    
100            if (other == null)
101                return false;
102    
103            if (_keys == null)
104                throw new IllegalStateException(Tapestry.getMessage("MultiKey.no-keys"));
105    
106            // Would a hashCode check be worthwhile here?
107    
108            try
109            {
110                MultiKey otherMulti = (MultiKey) other;
111    
112                if (_keys.length != otherMulti._keys.length) return false;
113    
114                for(i = 0; i < _keys.length; i++)
115                {
116                    // On an exact match, continue. This means that null matches
117                    // null.
118    
119                    if (_keys[i] == otherMulti._keys[i])
120                        continue;
121    
122                    // If either is null, but not both, then
123                    // not a match.
124    
125                    if (_keys[i] == null || otherMulti._keys[i] == null)
126                        return false;
127    
128                    if (!_keys[i].equals(otherMulti._keys[i]))
129                        return false;
130                }
131    
132                // Every key equal. A match.
133    
134                return true;
135            }
136            catch (ClassCastException e)
137            {
138            }
139    
140            return false;
141        }
142    
143        /**
144         * Returns the hash code of the receiver, which is computed from all the
145         * non-null key elements. This value is computed once and then cached, so
146         * elements should not change their hash codes once created (note that this
147         * is the same constraint that would be used if the individual key elements
148         * were themselves {@link java.util.Map} keys.
149         */
150    
151        public int hashCode()
152        {
153            if (hashCode == HASH_CODE_UNSET)
154            {
155                hashCode = _keys[0].hashCode();
156    
157                for(int i = 1; i < _keys.length; i++)
158                {
159                    if (_keys[i] != null) hashCode ^= _keys[i].hashCode();
160                }
161            }
162    
163            return hashCode;
164        }
165    
166        /**
167         * Identifies all the keys stored by this <code>MultiKey</code>.
168         */
169    
170        public String toString()
171        {
172            StringBuffer buffer;
173            int i;
174    
175            buffer = new StringBuffer("MultiKey[");
176    
177            for(i = 0; i < _keys.length; i++)
178            {
179                if (i > 0) buffer.append(", ");
180    
181                if (_keys[i] == null)
182                    buffer.append("<null>");
183                else buffer.append(_keys[i]);
184            }
185    
186            buffer.append(']');
187    
188            return buffer.toString();
189        }
190    
191        /**
192         * Writes a count of the keys, then writes each individual key.
193         */
194    
195        public void writeExternal(ObjectOutput out)
196            throws IOException
197        {
198            out.writeInt(_keys.length);
199    
200            for(int i = 0; i < _keys.length; i++)
201                out.writeObject(_keys[i]);
202        }
203    
204        /**
205         * Reads the state previously written by
206         * {@link #writeExternal(ObjectOutput)}.
207         */
208    
209        public void readExternal(ObjectInput in)
210            throws IOException, ClassNotFoundException
211        {
212            int count;
213    
214            count = in.readInt();
215            _keys = new Object[count];
216    
217            for(int i = 0; i < count; i++)
218                _keys[i] = in.readObject();
219        }
220    }