001/* 002 * 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019package org.apache.hadoop.hbase.filter; 020 021import java.io.IOException; 022import java.util.ArrayList; 023 024import org.apache.hadoop.hbase.Cell; 025import org.apache.hadoop.hbase.CellUtil; 026import org.apache.hadoop.hbase.PrivateCellUtil; 027import org.apache.yetus.audience.InterfaceAudience; 028import org.apache.hadoop.hbase.exceptions.DeserializationException; 029import org.apache.hadoop.hbase.shaded.protobuf.generated.FilterProtos; 030import org.apache.hadoop.hbase.util.Bytes; 031 032import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; 033import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException; 034import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations; 035 036/** 037 * A filter, based on the ColumnCountGetFilter, takes two arguments: limit and offset. 038 * This filter can be used for row-based indexing, where references to other tables are stored across many columns, 039 * in order to efficient lookups and paginated results for end users. Only most recent versions are considered 040 * for pagination. 041 */ 042@InterfaceAudience.Public 043public class ColumnPaginationFilter extends FilterBase { 044 045 private int limit = 0; 046 private int offset = -1; 047 private byte[] columnOffset = null; 048 private int count = 0; 049 050 /** 051 * Initializes filter with an integer offset and limit. The offset is arrived at 052 * scanning sequentially and skipping entries. @limit number of columns are 053 * then retrieved. If multiple column families are involved, the columns may be spread 054 * across them. 055 * 056 * @param limit Max number of columns to return. 057 * @param offset The integer offset where to start pagination. 058 */ 059 public ColumnPaginationFilter(final int limit, final int offset) 060 { 061 Preconditions.checkArgument(limit >= 0, "limit must be positive %s", limit); 062 Preconditions.checkArgument(offset >= 0, "offset must be positive %s", offset); 063 this.limit = limit; 064 this.offset = offset; 065 } 066 067 /** 068 * Initializes filter with a string/bookmark based offset and limit. The offset is arrived 069 * at, by seeking to it using scanner hints. If multiple column families are involved, 070 * pagination starts at the first column family which contains @columnOffset. Columns are 071 * then retrieved sequentially upto @limit number of columns which maybe spread across 072 * multiple column families, depending on how the scan is setup. 073 * 074 * @param limit Max number of columns to return. 075 * @param columnOffset The string/bookmark offset on where to start pagination. 076 */ 077 public ColumnPaginationFilter(final int limit, final byte[] columnOffset) { 078 Preconditions.checkArgument(limit >= 0, "limit must be positive %s", limit); 079 Preconditions.checkArgument(columnOffset != null, 080 "columnOffset must be non-null %s", 081 columnOffset); 082 this.limit = limit; 083 this.columnOffset = columnOffset; 084 } 085 086 /** 087 * @return limit 088 */ 089 public int getLimit() { 090 return limit; 091 } 092 093 /** 094 * @return offset 095 */ 096 public int getOffset() { 097 return offset; 098 } 099 100 /** 101 * @return columnOffset 102 */ 103 public byte[] getColumnOffset() { 104 return columnOffset; 105 } 106 107 @Override 108 public boolean filterRowKey(Cell cell) throws IOException { 109 // Impl in FilterBase might do unnecessary copy for Off heap backed Cells. 110 return false; 111 } 112 113 @Override 114 @Deprecated 115 public ReturnCode filterKeyValue(final Cell c) { 116 return filterCell(c); 117 } 118 119 @Override 120 public ReturnCode filterCell(final Cell c) 121 { 122 if (columnOffset != null) { 123 if (count >= limit) { 124 return ReturnCode.NEXT_ROW; 125 } 126 int cmp = 0; 127 // Only compare if no KV's have been seen so far. 128 if (count == 0) { 129 cmp = CellUtil.compareQualifiers(c, this.columnOffset, 0, this.columnOffset.length); 130 } 131 if (cmp < 0) { 132 return ReturnCode.SEEK_NEXT_USING_HINT; 133 } else { 134 count++; 135 return ReturnCode.INCLUDE_AND_NEXT_COL; 136 } 137 } else { 138 if (count >= offset + limit) { 139 return ReturnCode.NEXT_ROW; 140 } 141 142 ReturnCode code = count < offset ? ReturnCode.NEXT_COL : 143 ReturnCode.INCLUDE_AND_NEXT_COL; 144 count++; 145 return code; 146 } 147 } 148 149 @Override 150 public Cell getNextCellHint(Cell cell) { 151 return PrivateCellUtil.createFirstOnRowCol(cell, columnOffset, 0, columnOffset.length); 152 } 153 154 @Override 155 public void reset() 156 { 157 this.count = 0; 158 } 159 160 public static Filter createFilterFromArguments(ArrayList<byte []> filterArguments) { 161 Preconditions.checkArgument(filterArguments.size() == 2, 162 "Expected 2 but got: %s", filterArguments.size()); 163 int limit = ParseFilter.convertByteArrayToInt(filterArguments.get(0)); 164 int offset = ParseFilter.convertByteArrayToInt(filterArguments.get(1)); 165 return new ColumnPaginationFilter(limit, offset); 166 } 167 168 /** 169 * @return The filter serialized using pb 170 */ 171 @Override 172 public byte [] toByteArray() { 173 FilterProtos.ColumnPaginationFilter.Builder builder = 174 FilterProtos.ColumnPaginationFilter.newBuilder(); 175 builder.setLimit(this.limit); 176 if (this.offset >= 0) { 177 builder.setOffset(this.offset); 178 } 179 if (this.columnOffset != null) { 180 builder.setColumnOffset(UnsafeByteOperations.unsafeWrap(this.columnOffset)); 181 } 182 return builder.build().toByteArray(); 183 } 184 185 /** 186 * @param pbBytes A pb serialized {@link ColumnPaginationFilter} instance 187 * @return An instance of {@link ColumnPaginationFilter} made from <code>bytes</code> 188 * @throws DeserializationException 189 * @see #toByteArray 190 */ 191 public static ColumnPaginationFilter parseFrom(final byte [] pbBytes) 192 throws DeserializationException { 193 FilterProtos.ColumnPaginationFilter proto; 194 try { 195 proto = FilterProtos.ColumnPaginationFilter.parseFrom(pbBytes); 196 } catch (InvalidProtocolBufferException e) { 197 throw new DeserializationException(e); 198 } 199 if (proto.hasColumnOffset()) { 200 return new ColumnPaginationFilter(proto.getLimit(), 201 proto.getColumnOffset().toByteArray()); 202 } 203 return new ColumnPaginationFilter(proto.getLimit(),proto.getOffset()); 204 } 205 206 /** 207 * @param o the other filter to compare with 208 * @return true if and only if the fields of the filter that are serialized 209 * are equal to the corresponding fields in other. Used for testing. 210 */ 211 @Override 212 boolean areSerializedFieldsEqual(Filter o) { 213 if (o == this) return true; 214 if (!(o instanceof ColumnPaginationFilter)) return false; 215 216 ColumnPaginationFilter other = (ColumnPaginationFilter)o; 217 if (this.columnOffset != null) { 218 return this.getLimit() == other.getLimit() && 219 Bytes.equals(this.getColumnOffset(), other.getColumnOffset()); 220 } 221 return this.getLimit() == other.getLimit() && this.getOffset() == other.getOffset(); 222 } 223 224 @Override 225 public String toString() { 226 if (this.columnOffset != null) { 227 return (this.getClass().getSimpleName() + "(" + this.limit + ", " + 228 Bytes.toStringBinary(this.columnOffset) + ")"); 229 } 230 return String.format("%s (%d, %d)", this.getClass().getSimpleName(), 231 this.limit, this.offset); 232 } 233}