View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.filter;
20  
21  import com.google.common.base.Preconditions;
22  import com.google.protobuf.ByteString;
23  import com.google.protobuf.InvalidProtocolBufferException;
24  import org.apache.hadoop.classification.InterfaceAudience;
25  import org.apache.hadoop.classification.InterfaceStability;
26  import org.apache.hadoop.hbase.KeyValue;
27  import org.apache.hadoop.hbase.exceptions.DeserializationException;
28  import org.apache.hadoop.hbase.protobuf.generated.FilterProtos;
29  import org.apache.hadoop.hbase.util.Bytes;
30  
31  import java.util.ArrayList;
32  
33  /**
34   * A filter, based on the ColumnCountGetFilter, takes two arguments: limit and offset.
35   * This filter can be used for row-based indexing, where references to other tables are stored across many columns,
36   * in order to efficient lookups and paginated results for end users. Only most recent versions are considered
37   * for pagination.
38   */
39  @InterfaceAudience.Public
40  @InterfaceStability.Stable
41  public class ColumnPaginationFilter extends FilterBase
42  {
43    private int limit = 0;
44    private int offset = -1;
45    private byte[] columnOffset = null;
46    private int count = 0;
47  
48    /**
49     * Initializes filter with an integer offset and limit. The offset is arrived at
50     * scanning sequentially and skipping entries. @limit number of columns are
51     * then retrieved. If multiple column families are involved, the columns may be spread
52     * across them.
53     *
54     * @param limit Max number of columns to return.
55     * @param offset The integer offset where to start pagination.
56     */
57    public ColumnPaginationFilter(final int limit, final int offset)
58    {
59      Preconditions.checkArgument(limit >= 0, "limit must be positive %s", limit);
60      Preconditions.checkArgument(offset >= 0, "offset must be positive %s", offset);
61      this.limit = limit;
62      this.offset = offset;
63    }
64  
65    /**
66     * Initializes filter with a string/bookmark based offset and limit. The offset is arrived
67     * at, by seeking to it using scanner hints. If multiple column families are involved,
68     * pagination starts at the first column family which contains @columnOffset. Columns are
69     * then retrieved sequentially upto @limit number of columns which maybe spread across
70     * multiple column families, depending on how the scan is setup.
71     *
72     * @param limit Max number of columns to return.
73     * @param columnOffset The string/bookmark offset on where to start pagination.
74     */
75    public ColumnPaginationFilter(final int limit, final byte[] columnOffset) {
76      Preconditions.checkArgument(limit >= 0, "limit must be positive %s", limit);
77      Preconditions.checkArgument(columnOffset != null,
78                                  "columnOffset must be non-null %s",
79                                  columnOffset);
80      this.limit = limit;
81      this.columnOffset = columnOffset;
82    }
83  
84    /**
85     * @return limit
86     */
87    public int getLimit() {
88      return limit;
89    }
90  
91    /**
92     * @return offset
93     */
94    public int getOffset() {
95      return offset;
96    }
97  
98    /**
99     * @return columnOffset
100    */
101   public byte[] getColumnOffset() {
102     return columnOffset;
103   }
104 
105   @Override
106   public ReturnCode filterKeyValue(KeyValue v)
107   {
108     if (columnOffset != null) {
109       if (count >= limit) {
110         return ReturnCode.NEXT_ROW;
111       }
112       byte[] buffer = v.getBuffer();
113       if (buffer == null) {
114         return ReturnCode.SEEK_NEXT_USING_HINT;
115       }
116       int cmp = 0;
117       // Only compare if no KV's have been seen so far.
118       if (count == 0) {
119         cmp = Bytes.compareTo(buffer,
120                               v.getQualifierOffset(),
121                               v.getQualifierLength(),
122                               this.columnOffset,
123                               0,
124                               this.columnOffset.length);
125       }
126       if (cmp < 0) {
127         return ReturnCode.SEEK_NEXT_USING_HINT;
128       } else {
129         count++;
130         return ReturnCode.INCLUDE_AND_NEXT_COL;
131       }
132     } else {
133       if (count >= offset + limit) {
134         return ReturnCode.NEXT_ROW;
135       }
136 
137       ReturnCode code = count < offset ? ReturnCode.NEXT_COL :
138                                          ReturnCode.INCLUDE_AND_NEXT_COL;
139       count++;
140       return code;
141     }
142   }
143 
144   public KeyValue getNextKeyHint(KeyValue kv) {
145     return KeyValue.createFirstOnRow(
146         kv.getBuffer(), kv.getRowOffset(), kv.getRowLength(), kv.getBuffer(),
147         kv.getFamilyOffset(), kv.getFamilyLength(), columnOffset, 0, columnOffset.length);
148   }
149 
150   @Override
151   public void reset()
152   {
153     this.count = 0;
154   }
155 
156   public static Filter createFilterFromArguments(ArrayList<byte []> filterArguments) {
157     Preconditions.checkArgument(filterArguments.size() == 2,
158                                 "Expected 2 but got: %s", filterArguments.size());
159     int limit = ParseFilter.convertByteArrayToInt(filterArguments.get(0));
160     int offset = ParseFilter.convertByteArrayToInt(filterArguments.get(1));
161     return new ColumnPaginationFilter(limit, offset);
162   }
163 
164   /**
165    * @return The filter serialized using pb
166    */
167   public byte [] toByteArray() {
168     FilterProtos.ColumnPaginationFilter.Builder builder =
169       FilterProtos.ColumnPaginationFilter.newBuilder();
170     builder.setLimit(this.limit);
171     if (this.offset >= 0) {
172       builder.setOffset(this.offset);
173     }
174     if (this.columnOffset != null) {
175       builder.setColumnOffset(ByteString.copyFrom(this.columnOffset));
176     }
177     return builder.build().toByteArray();
178   }
179 
180   /**
181    * @param pbBytes A pb serialized {@link ColumnPaginationFilter} instance
182    * @return An instance of {@link ColumnPaginationFilter} made from <code>bytes</code>
183    * @throws DeserializationException
184    * @see #toByteArray
185    */
186   public static ColumnPaginationFilter parseFrom(final byte [] pbBytes)
187   throws DeserializationException {
188     FilterProtos.ColumnPaginationFilter proto;
189     try {
190       proto = FilterProtos.ColumnPaginationFilter.parseFrom(pbBytes);
191     } catch (InvalidProtocolBufferException e) {
192       throw new DeserializationException(e);
193     }
194     if (proto.hasColumnOffset()) {
195       return new ColumnPaginationFilter(proto.getLimit(),
196                                         proto.getColumnOffset().toByteArray());
197     }
198     return new ColumnPaginationFilter(proto.getLimit(),proto.getOffset());
199   }
200 
201   /**
202    * @param other
203    * @return true if and only if the fields of the filter that are serialized
204    * are equal to the corresponding fields in other.  Used for testing.
205    */
206   boolean areSerializedFieldsEqual(Filter o) {
207     if (o == this) return true;
208     if (!(o instanceof ColumnPaginationFilter)) return false;
209 
210     ColumnPaginationFilter other = (ColumnPaginationFilter)o;
211     if (this.columnOffset != null) {
212       return this.getLimit() == this.getLimit() &&
213           Bytes.equals(this.getColumnOffset(), other.getColumnOffset());
214     }
215     return this.getLimit() == other.getLimit() && this.getOffset() == other.getOffset();
216   }
217 
218   @Override
219   public String toString() {
220     if (this.columnOffset != null) {
221       return (this.getClass().getSimpleName() + "(" + this.limit + ", " +
222           Bytes.toStringBinary(this.columnOffset) + ")");
223     }
224     return String.format("%s (%d, %d)", this.getClass().getSimpleName(),
225         this.limit, this.offset);
226   }
227 }