summaryrefslogtreecommitdiff
path: root/src/main/java/com/amazon/carbonado/cursor/GroupedCursor.java
blob: 09952967d46cc0e555c15b1f15c583ac0fdffa76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/*
 * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates.
 * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.amazon.carbonado.cursor;

import java.util.Comparator;
import java.util.NoSuchElementException;

import com.amazon.carbonado.Cursor;
import com.amazon.carbonado.FetchException;

/**
 * Abstract cursor for aggregation and finding distinct data. The source cursor
 * must be ordered in some fashion by the grouping properties. The arrangement of
 * properties must match, but it does not matter if they are ascending or
 * descending.
 *
 * @author Brian S O'Neill
 * @see SortedCursor
 * @param <S> source type, can be anything
 * @param <G> aggregate type, can be anything
 */
public abstract class GroupedCursor<S, G> extends AbstractCursor<G> {
    private final Cursor<S> mCursor;
    private final Comparator<S> mGroupComparator;

    private S mGroupLeader;
    private G mNextAggregate;

    /**
     * Create a GroupedCursor with an existing group comparator. The comparator
     * defines the ordering of the source cursor, and it should be a partial
     * odering. If group comparator defines a total ordering, then all groups
     * have one member.
     *
     * @param cursor source of elements which must be ordered properly
     * @param groupComparator comparator which defines ordering of source cursor
     */
    protected GroupedCursor(Cursor<S> cursor, Comparator<S> groupComparator) {
        if (cursor == null || groupComparator == null) {
            throw new IllegalArgumentException();
        }
        mCursor = cursor;
        mGroupComparator = groupComparator;
    }

    /**
     * Create a GroupedCursor using properties to define the group
     * comparator. The set of properties defines the ordering of the source
     * cursor, and it should be a partial ordering. If properties define a
     * total ordering, then all groups have one member.
     *
     * @param cursor source of elements which must be ordered properly
     * @param type type of storable to create cursor for
     * @param groupProperties list of properties to group by
     * @throws IllegalArgumentException if any property is null or not a member
     * of storable type
     */
    protected GroupedCursor(Cursor<S> cursor, Class<S> type, String... groupProperties) {
        if (cursor == null) {
            throw new IllegalArgumentException();
        }
        mCursor = cursor;
        mGroupComparator = SortedCursor.createComparator(type, groupProperties);
    }

    /**
     * Returns the comparator used to identify group boundaries.
     */
    public Comparator<S> comparator() {
        return mGroupComparator;
    }

    /**
     * This method is called for the first entry in a group. This method is not
     * called again until after finishGroup is called.
     *
     * @param groupLeader first entry in group
     */
    protected abstract void beginGroup(S groupLeader) throws FetchException;

    /**
     * This method is called when more entries are found for the current
     * group. This method is not called until after beginGroup has been
     * called. It may called multiple times until finishGroup is called.
     *
     * @param groupMember additional entry in group
     */
    protected abstract void addToGroup(S groupMember) throws FetchException;

    /**
     * This method is called when a group is finished, and it can return an
     * aggregate. Simply return null if aggregate should be filtered out.
     *
     * @return aggregate, or null to filter it out
     */
    protected abstract G finishGroup() throws FetchException;

    public void close() throws FetchException {
        mCursor.close();
        mGroupLeader = null;
        mNextAggregate = null;
    }

    public boolean hasNext() throws FetchException {
        if (mNextAggregate != null) {
            return true;
        }

        try {
            if (mCursor.hasNext()) {
                if (mGroupLeader == null) {
                    beginGroup(mGroupLeader = mCursor.next());
                }

                while (mCursor.hasNext()) {
                    S groupMember = mCursor.next();

                    if (mGroupComparator.compare(mGroupLeader, groupMember) == 0) {
                        addToGroup(groupMember);
                    } else {
                        G aggregate = finishGroup();
                        beginGroup(mGroupLeader = groupMember);
                        if (aggregate != null) {
                            mNextAggregate = aggregate;
                            return true;
                        }
                    }
                }

                G aggregate = finishGroup();
                mGroupLeader = null;
                if (aggregate != null) {
                    mNextAggregate = aggregate;
                    return true;
                }
            }
        } catch (NoSuchElementException e) {
        } catch (FetchException e) {
            try {
                close();
            } catch (Exception e2) {
                // Don't care.
            }
            throw e;
        }

        if (mGroupLeader != null) {
            G aggregate = finishGroup();
            mGroupLeader = null;
            if (aggregate != null) {
                mNextAggregate = aggregate;
                return true;
            }
        }

        return false;
    }

    public G next() throws FetchException {
        try {
            if (hasNext()) {
                G next = mNextAggregate;
                mNextAggregate = null;
                return next;
            }
        } catch (FetchException e) {
            try {
                close();
            } catch (Exception e2) {
                // Don't care.
            }
            throw e;
        }
        throw new NoSuchElementException();
    }

    @Override
    public int skipNext(int amount) throws FetchException {
        if (amount <= 0) {
            if (amount < 0) {
                throw new IllegalArgumentException("Cannot skip negative amount: " + amount);
            }
            return 0;
        }

        try {
            int count = 0;
            while (--amount >= 0 && hasNext()) {
                ++count;
                mNextAggregate = null;
            }

            return count;
        } catch (FetchException e) {
            try {
                close();
            } catch (Exception e2) {
                // Don't care.
            }
            throw e;
        }
    }
}