1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2002,2008 Oracle.  All rights reserved.
5 *
6 * $Id: EntityJoin.java,v 1.1 2008/02/07 17:12:26 mark Exp $
7 */
8
9package com.sleepycat.persist;
10
11import java.util.ArrayList;
12import java.util.Iterator;
13import java.util.List;
14
15import com.sleepycat.bind.EntityBinding;
16import com.sleepycat.bind.EntryBinding;
17import com.sleepycat.db.Cursor;
18import com.sleepycat.db.CursorConfig;
19import com.sleepycat.db.Database;
20import com.sleepycat.db.DatabaseEntry;
21import com.sleepycat.db.DatabaseException;
22import com.sleepycat.db.JoinCursor;
23import com.sleepycat.db.LockMode;
24import com.sleepycat.db.OperationStatus;
25import com.sleepycat.db.Transaction;
26
27/**
28 * Performs an equality join on two or more secondary keys.
29 *
30 * <p>{@code EntityJoin} objects are thread-safe.  Multiple threads may safely
31 * call the methods of a shared {@code EntityJoin} object.</p>
32 *
33 * <p>An equality join is a match on all entities in a given primary index that
34 * have two or more specific secondary key values.  Note that key ranges may
35 * not be matched by an equality join, only exact keys are matched.</p>
36 *
37 * <p>For example:</p>
38 * <pre class="code">
39 *  // Index declarations -- see {@link <a href="package-summary.html#example">package summary example</a>}.
40 *  //
41 *  {@literal PrimaryIndex<String,Person> personBySsn;}
42 *  {@literal SecondaryIndex<String,String,Person> personByParentSsn;}
43 *  {@literal SecondaryIndex<Long,String,Person> personByEmployerIds;}
44 *  Employer employer = ...;
45 *
46 *  // Match on all Person objects having parentSsn "111-11-1111" and also
47 *  // containing an employerId of employer.id.  In other words, match on all
48 *  // of Bob's children that work for a given employer.
49 *  //
50 *  {@literal EntityJoin<String,Person> join = new EntityJoin(personBySsn);}
51 *  join.addCondition(personByParentSsn, "111-11-1111");
52 *  join.addCondition(personByEmployerIds, employer.id);
53 *
54 *  // Perform the join operation by traversing the results with a cursor.
55 *  //
56 *  {@literal ForwardCursor<Person> results = join.entities();}
57 *  try {
58 *      for (Person person : results) {
59 *          System.out.println(person.ssn + ' ' + person.name);
60 *      }
61 *  } finally {
62 *      results.close();
63 *  }</pre>
64 *
65 * @author Mark Hayes
66 */
67public class EntityJoin<PK,E> {
68
69    private PrimaryIndex<PK,E> primary;
70    private List<Condition> conditions;
71
72    /**
73     * Creates a join object for a given primary index.
74     *
75     * @param index the primary index on which the join will operate.
76     */
77    public EntityJoin(PrimaryIndex<PK,E> index) {
78        primary = index;
79        conditions = new ArrayList<Condition>();
80    }
81
82    /**
83     * Adds a secondary key condition to the equality join.  Only entities
84     * having the given key value in the given secondary index will be returned
85     * by the join operation.
86     *
87     * @param index the secondary index containing the given key value.
88     *
89     * @param key the key value to match during the join.
90     */
91    public <SK> void addCondition(SecondaryIndex<SK,PK,E> index, SK key) {
92
93        /* Make key entry. */
94        DatabaseEntry keyEntry = new DatabaseEntry();
95        index.getKeyBinding().objectToEntry(key, keyEntry);
96
97        /* Use keys database if available. */
98        Database db = index.getKeysDatabase();
99        if (db == null) {
100            db = index.getDatabase();
101        }
102
103        /* Add condition. */
104        conditions.add(new Condition(db, keyEntry));
105    }
106
107    /**
108     * Opens a cursor that returns the entities qualifying for the join.  The
109     * join operation is performed as the returned cursor is accessed.
110     *
111     * <p>The operations performed with the cursor will not be transaction
112     * protected, and {@link CursorConfig#DEFAULT} is used implicitly.</p>
113     *
114     * @return the cursor.
115     *
116     * @throws IllegalStateException if less than two conditions were added.
117     */
118    public ForwardCursor<E> entities()
119        throws DatabaseException {
120
121        return entities(null, null);
122    }
123
124    /**
125     * Opens a cursor that returns the entities qualifying for the join.  The
126     * join operation is performed as the returned cursor is accessed.
127     *
128     * @param txn the transaction used to protect all operations performed with
129     * the cursor, or null if the operations should not be transaction
130     * protected.
131     *
132     * @param config the cursor configuration that determines the default lock
133     * mode used for all cursor operations, or null to implicitly use {@link
134     * CursorConfig#DEFAULT}.
135     *
136     * @return the cursor.
137     *
138     * @throws IllegalStateException if less than two conditions were added.
139     */
140    public ForwardCursor<E> entities(Transaction txn, CursorConfig config)
141        throws DatabaseException {
142
143        return new JoinForwardCursor<E>(txn, config, false);
144    }
145
146    /**
147     * Opens a cursor that returns the primary keys of entities qualifying for
148     * the join.  The join operation is performed as the returned cursor is
149     * accessed.
150     *
151     * <p>The operations performed with the cursor will not be transaction
152     * protected, and {@link CursorConfig#DEFAULT} is used implicitly.</p>
153     *
154     * @return the cursor.
155     *
156     * @throws IllegalStateException if less than two conditions were added.
157     */
158    public ForwardCursor<PK> keys()
159        throws DatabaseException {
160
161        return keys(null, null);
162    }
163
164    /**
165     * Opens a cursor that returns the primary keys of entities qualifying for
166     * the join.  The join operation is performed as the returned cursor is
167     * accessed.
168     *
169     * @param txn the transaction used to protect all operations performed with
170     * the cursor, or null if the operations should not be transaction
171     * protected.
172     *
173     * @param config the cursor configuration that determines the default lock
174     * mode used for all cursor operations, or null to implicitly use {@link
175     * CursorConfig#DEFAULT}.
176     *
177     * @return the cursor.
178     *
179     * @throws IllegalStateException if less than two conditions were added.
180     */
181    public ForwardCursor<PK> keys(Transaction txn, CursorConfig config)
182        throws DatabaseException {
183
184        return new JoinForwardCursor<PK>(txn, config, true);
185    }
186
187    private static class Condition {
188
189        private Database db;
190        private DatabaseEntry key;
191
192        Condition(Database db, DatabaseEntry key) {
193            this.db = db;
194            this.key = key;
195        }
196
197        Cursor openCursor(Transaction txn, CursorConfig config)
198            throws DatabaseException {
199
200            OperationStatus status;
201            Cursor cursor = db.openCursor(txn, config);
202            try {
203                DatabaseEntry data = BasicIndex.NO_RETURN_ENTRY;
204                status = cursor.getSearchKey(key, data, null);
205            } catch (DatabaseException e) {
206                try {
207                    cursor.close();
208                } catch (DatabaseException ignored) {}
209                throw e;
210            }
211            if (status == OperationStatus.SUCCESS) {
212                return cursor;
213            } else {
214                cursor.close();
215                return null;
216            }
217        }
218    }
219
220    private class JoinForwardCursor<V> implements ForwardCursor<V> {
221
222        private Cursor[] cursors;
223        private JoinCursor joinCursor;
224        private boolean doKeys;
225
226        JoinForwardCursor(Transaction txn, CursorConfig config, boolean doKeys)
227            throws DatabaseException {
228
229            this.doKeys = doKeys;
230            try {
231                cursors = new Cursor[conditions.size()];
232                for (int i = 0; i < cursors.length; i += 1) {
233                    Condition cond = conditions.get(i);
234                    Cursor cursor = cond.openCursor(txn, config);
235                    if (cursor == null) {
236                        /* Leave joinCursor null. */
237                        doClose(null);
238                        return;
239                    }
240                    cursors[i] = cursor;
241                }
242                joinCursor = primary.getDatabase().join(cursors, null);
243            } catch (DatabaseException e) {
244                /* doClose will throw e. */
245                doClose(e);
246            }
247        }
248
249        public V next()
250            throws DatabaseException {
251
252            return next(null);
253        }
254
255        public V next(LockMode lockMode)
256            throws DatabaseException {
257
258            if (joinCursor == null) {
259                return null;
260            }
261            if (doKeys) {
262                DatabaseEntry key = new DatabaseEntry();
263                OperationStatus status = joinCursor.getNext(key, lockMode);
264                if (status == OperationStatus.SUCCESS) {
265                    EntryBinding binding = primary.getKeyBinding();
266                    return (V) binding.entryToObject(key);
267                }
268            } else {
269                DatabaseEntry key = new DatabaseEntry();
270                DatabaseEntry data = new DatabaseEntry();
271                OperationStatus status =
272                    joinCursor.getNext(key, data, lockMode);
273                if (status == OperationStatus.SUCCESS) {
274                    EntityBinding binding = primary.getEntityBinding();
275                    return (V) binding.entryToObject(key, data);
276                }
277            }
278            return null;
279        }
280
281        public Iterator<V> iterator() {
282            return iterator(null);
283        }
284
285        public Iterator<V> iterator(LockMode lockMode) {
286            return new BasicIterator<V>(this, lockMode);
287        }
288
289        public void close()
290            throws DatabaseException {
291
292            doClose(null);
293        }
294
295        private void doClose(DatabaseException firstException)
296            throws DatabaseException {
297
298            if (joinCursor != null) {
299                try {
300                    joinCursor.close();
301                    joinCursor = null;
302                } catch (DatabaseException e) {
303                    if (firstException == null) {
304                        firstException = e;
305                    }
306                }
307            }
308            for (int i = 0; i < cursors.length; i += 1) {
309                Cursor cursor = cursors[i];
310                if (cursor != null) {
311                    try {
312                        cursor.close();
313                        cursors[i] = null;
314                    } catch (DatabaseException e) {
315                        if (firstException == null) {
316                            firstException = e;
317                        }
318                    }
319                }
320            }
321            if (firstException != null) {
322                throw firstException;
323            }
324        }
325    }
326}
327