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
015package org.apache.tapestry.util;
016
017import java.io.Externalizable;
018import java.io.IOException;
019import java.io.ObjectInput;
020import java.io.ObjectOutput;
021
022import 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
032public 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}