View Javadoc

1   /*
2    * Copyright (c) 2004 UNINETT FAS
3    *
4    * This program is free software; you can redistribute it and/or modify it
5    * under the terms of the GNU General Public License as published by the Free
6    * Software Foundation; either version 2 of the License, or (at your option)
7    * any later version.
8    *
9    * This program is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12   * more details.
13   *
14   * You should have received a copy of the GNU General Public License along with
15   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
16   * Place - Suite 330, Boston, MA 02111-1307, USA.
17   *
18   */
19  
20  package no.feide.moria.directory.index;
21  
22  import java.io.Serializable;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  
28  /***
29   * A serializable index implementation, used for offline generation of a new
30   * index.
31   */
32  public class SerializableIndex
33  implements Serializable, DirectoryManagerIndex {
34  
35      /*** Serial version identifier. */
36      static final long serialVersionUID = -3356791609795577197L;
37  
38      /***
39       * Internal list of associations; that is, the mapping between logical ID
40       * realms (as <code>String</code>s) - following the 'at' character - and
41       * search base references (as <code>String</code> arrays).
42       */
43      private HashMap associations = new HashMap();
44  
45      /***
46       * Internal list of exceptions to the associations; that is, explicitly
47       * indexed logical IDs (as <code>String</code>s) to full external
48       * references (as <code>String</code> s).
49       */
50      private HashMap exceptions = new HashMap();
51  
52      /***
53       * Internal list of realms for each exception. The list should have the same
54       * keys as <code>exceptions</code>, but the elements of this list give
55       * the explicit realm (as <code>String</code>s) for each exception.
56       */
57      private HashMap realms = new HashMap();
58  
59      /***
60       * Internal list of usernames for each association/exception.
61       */
62      private HashMap usernames = new HashMap();
63  
64      /***
65       * Internal list of passwords for each association/exception.
66       */
67      private HashMap passwords = new HashMap();
68  
69  
70      /***
71       * Checks whether two index instances are equal. <br>
72       * <br>
73       * Note that for convenience (and/or laziness) this method relies on the
74       * <code>String</code> representation as given by <code>toString()</code>.
75       * @param obj
76       *            The other <code>SerializableIndex</code> object to compare
77       *            to.
78       * @return <code>true</code> if two <code>SerializableIndex</code>
79       *         objects are equal, otherwise <code>false</code>. Two instances
80       *         are equal if and only if their lists of associations, exceptions
81       *         and realms are equal.
82       * @see java.lang.Object#equals(java.lang.Object)
83       * @see #toString()
84       */
85      public final boolean equals(final Object obj) {
86  
87          // Check class.
88          if (obj.getClass() != this.getClass())
89              return false;
90          final SerializableIndex other = (SerializableIndex) obj;
91  
92          // Check associations. Normal equals(...) doesn't work here.
93          if (!associations.keySet().equals(other.associations.keySet()))
94              return false;
95          Iterator keys = associations.keySet().iterator();
96          while (keys.hasNext()) {
97              String key = (String) keys.next();
98              String[] values = (String[]) associations.get(key);
99              ArrayList myValues = new ArrayList(values.length);
100             for (int i = 0; i < values.length; i++)
101                 myValues.add(values[i]);
102             values = (String[]) other.associations.get(key);
103             ArrayList otherValues = new ArrayList(values.length);
104             for (int i = 0; i < values.length; i++)
105                 otherValues.add(values[i]);
106             if (!myValues.equals(otherValues))
107                 return false;
108         }
109 
110         // Check exceptions, realms, usernames and passwords; much simple data
111         // structure.
112         if (!exceptions.equals(other.exceptions))
113             return false;
114         if (!realms.equals(other.realms))
115             return false;
116         if (!usernames.equals(other.usernames))
117             return false;
118         if (!passwords.equals(other.passwords))
119             return false;
120 
121         // We're okay.
122         return true;
123     }
124 
125 
126     /***
127      * Generates a hashCode. Dummy implementation.
128      * @return The hashcode.
129      * @throws UnsupportedOperationException
130      *             Always.
131      */
132     public final int hashCode() {
133 
134         throw new UnsupportedOperationException();
135     }
136 
137 
138     /***
139      * Looks up an element reference from the index based on its logical ID
140      * (typically username). <br>
141      * <br>
142      * Note that looking up in the association list requires the logical ID to
143      * be on the form <code>identifier-at-realm</code>, similar to an email
144      * address. This is <em>not</em> a requirement for looking up references
145      * in the exception list, and therefore the 'at' character is not required
146      * in the logical ID.
147      * @param id
148      *            The logical identifier to look up.
149      * @return One or more references matching the given identifier, or
150      *         <code>null</code> if no such reference was found.
151      * @see DirectoryManagerIndex#getReferences(String)
152      */
153     public final IndexedReference[] getReferences(final String id) {
154 
155         // Sanity check.
156         if (id == null)
157             return null;
158 
159         ArrayList newReferences = new ArrayList();
160 
161         // Do we have an explicit match? That is, an exception from the
162         // association rule?
163         if (exceptions.containsKey(id))
164             newReferences.add(new IndexedReference(new String[] {(String) exceptions.get(id)}, new String[] {""}, new String[] {""}, true));
165 
166         // Extract the realm, with sanity check.
167         int i = id.lastIndexOf('@');
168         if ((i > 0) && (associations.containsKey(id.substring(i + 1)))) {
169 
170             // Gather associations, usernames and passwords.
171             String[] gatheredAssociations = (String[]) associations.get(id.substring(i + 1));
172             ArrayList gatheredUsernames = new ArrayList(gatheredAssociations.length);
173             ArrayList gatheredPasswords = new ArrayList(gatheredAssociations.length);
174             for (int j = 0; j < gatheredAssociations.length; j++) {
175                 gatheredUsernames.add(usernames.get(gatheredAssociations[j]));
176                 gatheredPasswords.add(passwords.get(gatheredAssociations[j]));
177             }
178             newReferences.add(new IndexedReference(gatheredAssociations, (String[]) gatheredUsernames.toArray(new String[] {}), (String[]) gatheredPasswords.toArray(new String[] {}), false));
179         }
180 
181         // Did we find any references?
182         if (newReferences.size() == 0)
183             return null;
184         return (IndexedReference[]) newReferences.toArray(new IndexedReference[] {});
185 
186     }
187 
188 
189     /***
190      * Looks up which realm a given identifier belongs to.
191      * @param id
192      *            The logical identifier to get realm for.
193      * @return The resolved realm for this identifier, or <code>null</code> if
194      *         no such realm could be found.
195      * @see DirectoryManagerIndex#getRealm(String)
196      */
197     public final String getRealm(final String id) {
198 
199         // Do we have an exception matching this identifier with an explicit
200         // realm?
201         if (realms.containsKey(id))
202             return (String) realms.get(id);
203 
204         // Do we have any associations for this realm?
205         int i = id.lastIndexOf('@');
206         if ((i > 0) && (associations.containsKey(id.substring(i + 1))))
207             return id.substring(i + 1);
208 
209         // No exception/realm and no association.
210         return null;
211 
212     }
213 
214 
215     /***
216      * Adds a new realm-to-base association to the index. Any modification of
217      * the index will result in any existing association with the same realm to
218      * be appended with the new realm. <br>
219      * <br>
220      * Note that this method does <em>not</em> check for duplicate
221      * associations (associations between one realm and two identical bases).
222      * @param realm
223      *            The realm (typically user realm) related to this base. Cannot
224      *            be <code>null</code>.
225      * @param base
226      *            The association. In practical use this will be an LDAP search
227      *            base similar to
228      *            <code>ldap://some.ldap.server:636/dc=search,dc=base</code>.
229      *            Cannot be <code>null</code>.
230      * @param username
231      *            The username to be used when performing searches on this
232      *            association's base. May not be <code>null</code>.
233      * @param password
234      *            The password to be used when performing searches on this
235      *            association's base. May not be <code>null</code>.
236      * @throws IllegalArgumentException
237      *             If either <code>realm</code>,<code>base</code>,
238      *             <code>username</code>, or <code>password</code> is
239      *             <code>null</code>.
240      */
241     public final void addAssociation(final String realm,
242                                      final String base,
243                                      final String username,
244                                      final String password) {
245 
246         // Sanity checks.
247         if (realm == null)
248             throw new IllegalArgumentException("Realm cannot be NULL");
249         if (base == null)
250             throw new IllegalArgumentException("Base cannot be NULL");
251         if (username == null)
252             throw new IllegalArgumentException("Username cannot be NULL");
253         if (password == null)
254             throw new IllegalArgumentException("Password cannot be NULL");
255 
256         // New association or updating an existing?
257         if (associations.containsKey(realm)) {
258 
259             // Update existing association.
260             ArrayList bases = new ArrayList(Arrays.asList((String[]) associations.get(realm)));
261             bases.add(base);
262             associations.put(realm, bases.toArray(new String[] {}));
263 
264         } else {
265 
266             // Create new association.
267             associations.put(realm, new String[] {base});
268 
269         }
270 
271         // Add credentials.
272         usernames.put(base, username);
273         passwords.put(base, password);
274 
275     }
276 
277 
278     /***
279      * Add a new search exception (exception to the basic rule of realm-to-base
280      * associations) to this index. Any modifications to an already existing
281      * exception will result in the old references being replaced.
282      * @param id
283      *            The identifier for this exception, typically a user ID. Cannot
284      *            be <code>null</code>.
285      * @param reference
286      *            The reference. In practical use this will be an LDAP element
287      *            reference similar to
288      *            <code>ldap://some.ldap.server:636/uid=id,dc=search,dc=base</code>.
289      *            Cannot be <code>null</code>.
290      * @param realm
291      *            The actual realm of the reference, which may not be given by
292      *            the identifier (on the form <i>user@realm </i>, for example).
293      *            Since moving between realms while keeping an unchanged
294      *            identifier is possible, this must be taken into account.
295      * @throws IllegalArgumentException
296      *             If either <code>id</code> or <code>reference</code> is
297      *             <code>null</code>.
298      */
299     public final void addException(final String id,
300                                    final String reference,
301                                    final String realm) {
302 
303         // Sanity checks.
304         if (id == null)
305             throw new IllegalArgumentException("ID cannot be NULL");
306         if (reference == null)
307             throw new IllegalArgumentException("Reference cannot be NULL");
308 
309         exceptions.put(id, reference);
310         realms.put(id, realm);
311 
312     }
313 
314 
315     /***
316      * Gives a string representation of the object, for visual debugging.
317      * @return The object represented as a <code>String</code>, includes
318      *         separate lists of associations and exceptions.
319      */
320     public final String toString() {
321 
322         // Associations.
323         String s = "\tAssociations: {";
324         Iterator associationKeys = associations.keySet().iterator();
325         while (associationKeys.hasNext()) {
326             String[] bases = (String[]) associations.get(associationKeys.next());
327             for (int i = 0; i < bases.length; i++)
328                 s = s + bases[i] + " '" + usernames.get(bases[i]) + "'/'" + passwords.get(bases[i]) + "'\n\t               ";
329         }
330         s = s.substring(0, s.length() - 17) + "}";
331 
332         // Exceptions.
333         return s = s + "\n\tExceptions: " + exceptions.toString().replaceAll(", ", "\n\t             ");
334 
335     }
336 
337 
338     /***
339      * Return the username associated with a given base.
340      * @param base
341      *            The search base. May not be <code>null</code> or an empty
342      *            string.
343      * @return The username associated with <code>base</code>. May be an
344      *         empty string, but will never be <code>null</code> even though
345      *         the <code>base</code> does not exist.
346      * @throws IllegalArgumentException
347      *             If <code>base</code> is <code>null</code> or an empty
348      *             string.
349      */
350     public final String getUsername(final String base) throws IllegalArgumentException {
351 
352         // Sanity check.
353         if ((base == null) || (base.length() == 0))
354             throw new IllegalArgumentException("Base must be a non-empty string");
355 
356         // Return the username, or an empty string if the base was not
357         // recognized.
358         if (usernames.containsKey(base))
359             return (String) usernames.get(base);
360         return "";
361 
362     }
363 
364 
365     /***
366      * Return the password associated with a given base.
367      * @param base
368      *            The search base. May not be <code>null</code> or an empty
369      *            string.
370      * @return The password associated with <code>base</code>. May be an
371      *         empty string, but will never be <code>null</code> even though
372      *         the <code>base</code> does not exist.
373      * @throws IllegalArgumentException
374      *             If <code>base</code> is <code>null</code> or an empty
375      *             string.
376      */
377     public final String getPassword(final String base) throws IllegalArgumentException {
378 
379         // Sanity check.
380         if ((base == null) || (base.length() == 0))
381             throw new IllegalArgumentException("Base must be a non-empty string");
382 
383         // Return the username, or an empty string if the base was not
384         // recognized.
385         if (passwords.containsKey(base))
386             return (String) passwords.get(base);
387         return "";
388 
389     }
390 
391 }