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