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 java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.List;
25  
26  import static org.junit.Assert.assertEquals;
27  import static org.junit.Assert.assertFalse;
28  import static org.junit.Assert.assertNotNull;
29  import static org.junit.Assert.assertTrue;
30  import static org.junit.Assert.assertNull;
31  
32  import org.apache.hadoop.hbase.Cell;
33  import org.apache.hadoop.hbase.KeyValue;
34  import org.apache.hadoop.hbase.KeyValueUtil;
35  import org.apache.hadoop.hbase.testclassification.SmallTests;
36  import org.apache.hadoop.hbase.exceptions.DeserializationException;
37  import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
38  import org.apache.hadoop.hbase.filter.Filter.ReturnCode;
39  import org.apache.hadoop.hbase.filter.FilterList.Operator;
40  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
41  import org.apache.hadoop.hbase.util.Bytes;
42  import org.junit.Test;
43  import org.junit.experimental.categories.Category;
44  
45  import com.google.common.collect.Lists;
46  
47  /**
48   * Tests filter sets
49   *
50   */
51  @Category(SmallTests.class)
52  public class TestFilterList {
53    static final int MAX_PAGES = 2;
54    static final char FIRST_CHAR = 'a';
55    static final char LAST_CHAR = 'e';
56    static byte[] GOOD_BYTES = Bytes.toBytes("abc");
57    static byte[] BAD_BYTES = Bytes.toBytes("def");
58  
59  
60    @Test
61    public void testAddFilter() throws Exception {
62      Filter filter1 = new FirstKeyOnlyFilter();
63      Filter filter2 = new FirstKeyOnlyFilter();
64  
65      FilterList filterList = new FilterList(filter1, filter2);
66      filterList.addFilter(new FirstKeyOnlyFilter());
67  
68      filterList = new FilterList(Arrays.asList(filter1, filter2));
69      filterList.addFilter(new FirstKeyOnlyFilter());
70  
71      filterList = new FilterList(Operator.MUST_PASS_ALL, filter1, filter2);
72      filterList.addFilter(new FirstKeyOnlyFilter());
73  
74      filterList = new FilterList(Operator.MUST_PASS_ALL, Arrays.asList(filter1, filter2));
75      filterList.addFilter(new FirstKeyOnlyFilter());
76  
77    }
78  
79  
80    /**
81     * Test "must pass one"
82     * @throws Exception
83     */
84    @Test
85    public void testMPONE() throws Exception {
86      mpOneTest(getFilterMPONE());
87    }
88  
89    private Filter getFilterMPONE() {
90      List<Filter> filters = new ArrayList<Filter>();
91      filters.add(new PageFilter(MAX_PAGES));
92      filters.add(new WhileMatchFilter(new PrefixFilter(Bytes.toBytes("yyy"))));
93      Filter filterMPONE =
94        new FilterList(FilterList.Operator.MUST_PASS_ONE, filters);
95      return filterMPONE;
96    }
97  
98    private void mpOneTest(Filter filterMPONE) throws Exception {
99      /* Filter must do all below steps:
100      * <ul>
101      * <li>{@link #reset()}</li>
102      * <li>{@link #filterAllRemaining()} -> true indicates scan is over, false, keep going on.</li>
103      * <li>{@link #filterRowKey(byte[],int,int)} -> true to drop this row,
104      * if false, we will also call</li>
105      * <li>{@link #filterKeyValue(org.apache.hadoop.hbase.KeyValue)} -> true to drop this key/value</li>
106      * <li>{@link #filterRow()} -> last chance to drop entire row based on the sequence of
107      * filterValue() calls. Eg: filter a row if it doesn't contain a specified column.
108      * </li>
109      * </ul>
110     */
111     filterMPONE.reset();
112     assertFalse(filterMPONE.filterAllRemaining());
113 
114     /* Will pass both */
115     byte [] rowkey = Bytes.toBytes("yyyyyyyyy");
116     for (int i = 0; i < MAX_PAGES - 1; i++) {
117       assertFalse(filterMPONE.filterRowKey(rowkey, 0, rowkey.length));
118       KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i),
119         Bytes.toBytes(i));
120       assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterKeyValue(kv));
121       assertFalse(filterMPONE.filterRow());
122     }
123 
124     /* Only pass PageFilter */
125     rowkey = Bytes.toBytes("z");
126     assertFalse(filterMPONE.filterRowKey(rowkey, 0, rowkey.length));
127     KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(0),
128         Bytes.toBytes(0));
129     assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterKeyValue(kv));
130     assertFalse(filterMPONE.filterRow());
131 
132     /* reach MAX_PAGES already, should filter any rows */
133     rowkey = Bytes.toBytes("yyy");
134     assertTrue(filterMPONE.filterRowKey(rowkey, 0, rowkey.length));
135     kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(0),
136         Bytes.toBytes(0));
137     assertFalse(Filter.ReturnCode.INCLUDE == filterMPONE.filterKeyValue(kv));
138     assertFalse(filterMPONE.filterRow());
139 
140     /* We should filter any row */
141     rowkey = Bytes.toBytes("z");
142     assertTrue(filterMPONE.filterRowKey(rowkey, 0, rowkey.length));
143     assertTrue(filterMPONE.filterAllRemaining());
144   }
145 
146   /**
147    * Test "must pass all"
148    * @throws Exception
149    */
150   @Test
151   public void testMPALL() throws Exception {
152     mpAllTest(getMPALLFilter());
153   }
154 
155   private Filter getMPALLFilter() {
156     List<Filter> filters = new ArrayList<Filter>();
157     filters.add(new PageFilter(MAX_PAGES));
158     filters.add(new WhileMatchFilter(new PrefixFilter(Bytes.toBytes("yyy"))));
159     Filter filterMPALL =
160       new FilterList(FilterList.Operator.MUST_PASS_ALL, filters);
161     return filterMPALL;
162   }
163 
164   private void mpAllTest(Filter filterMPALL) throws Exception {
165     /* Filter must do all below steps:
166      * <ul>
167      * <li>{@link #reset()}</li>
168      * <li>{@link #filterAllRemaining()} -> true indicates scan is over, false, keep going on.</li>
169      * <li>{@link #filterRowKey(byte[],int,int)} -> true to drop this row,
170      * if false, we will also call</li>
171      * <li>{@link #filterKeyValue(org.apache.hadoop.hbase.KeyValue)} -> true to drop this key/value</li>
172      * <li>{@link #filterRow()} -> last chance to drop entire row based on the sequence of
173      * filterValue() calls. Eg: filter a row if it doesn't contain a specified column.
174      * </li>
175      * </ul>
176     */
177     filterMPALL.reset();
178     assertFalse(filterMPALL.filterAllRemaining());
179     byte [] rowkey = Bytes.toBytes("yyyyyyyyy");
180     for (int i = 0; i < MAX_PAGES - 1; i++) {
181       assertFalse(filterMPALL.filterRowKey(rowkey, 0, rowkey.length));
182       KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i),
183         Bytes.toBytes(i));
184       assertTrue(Filter.ReturnCode.INCLUDE == filterMPALL.filterKeyValue(kv));
185     }
186     filterMPALL.reset();
187     rowkey = Bytes.toBytes("z");
188     assertTrue(filterMPALL.filterRowKey(rowkey, 0, rowkey.length));
189     // Should fail here; row should be filtered out.
190     KeyValue kv = new KeyValue(rowkey, rowkey, rowkey, rowkey);
191     assertTrue(Filter.ReturnCode.NEXT_ROW == filterMPALL.filterKeyValue(kv));
192   }
193 
194   /**
195    * Test list ordering
196    * @throws Exception
197    */
198   @Test
199   public void testOrdering() throws Exception {
200     orderingTest(getOrderingFilter());
201   }
202 
203   public Filter getOrderingFilter() {
204     List<Filter> filters = new ArrayList<Filter>();
205     filters.add(new PrefixFilter(Bytes.toBytes("yyy")));
206     filters.add(new PageFilter(MAX_PAGES));
207     Filter filterMPONE =
208       new FilterList(FilterList.Operator.MUST_PASS_ONE, filters);
209     return filterMPONE;
210   }
211 
212   public void orderingTest(Filter filterMPONE) throws Exception {
213     /* Filter must do all below steps:
214      * <ul>
215      * <li>{@link #reset()}</li>
216      * <li>{@link #filterAllRemaining()} -> true indicates scan is over, false, keep going on.</li>
217      * <li>{@link #filterRowKey(byte[],int,int)} -> true to drop this row,
218      * if false, we will also call</li>
219      * <li>{@link #filterKeyValue(org.apache.hadoop.hbase.KeyValue)} -> true to drop this key/value</li>
220      * <li>{@link #filterRow()} -> last chance to drop entire row based on the sequence of
221      * filterValue() calls. Eg: filter a row if it doesn't contain a specified column.
222      * </li>
223      * </ul>
224     */
225     filterMPONE.reset();
226     assertFalse(filterMPONE.filterAllRemaining());
227 
228     /* We should be able to fill MAX_PAGES without incrementing page counter */
229     byte [] rowkey = Bytes.toBytes("yyyyyyyy");
230     for (int i = 0; i < MAX_PAGES; i++) {
231       assertFalse(filterMPONE.filterRowKey(rowkey, 0, rowkey.length));
232       KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i),
233           Bytes.toBytes(i));
234         assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterKeyValue(kv));
235       assertFalse(filterMPONE.filterRow());
236     }
237 
238     /* Now let's fill the page filter */
239     rowkey = Bytes.toBytes("xxxxxxx");
240     for (int i = 0; i < MAX_PAGES; i++) {
241       assertFalse(filterMPONE.filterRowKey(rowkey, 0, rowkey.length));
242       KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i),
243           Bytes.toBytes(i));
244         assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterKeyValue(kv));
245       assertFalse(filterMPONE.filterRow());
246     }
247 
248     /* We should still be able to include even though page filter is at max */
249     rowkey = Bytes.toBytes("yyy");
250     for (int i = 0; i < MAX_PAGES; i++) {
251       assertFalse(filterMPONE.filterRowKey(rowkey, 0, rowkey.length));
252       KeyValue kv = new KeyValue(rowkey, rowkey, Bytes.toBytes(i),
253           Bytes.toBytes(i));
254         assertTrue(Filter.ReturnCode.INCLUDE == filterMPONE.filterKeyValue(kv));
255       assertFalse(filterMPONE.filterRow());
256     }
257   }
258 
259   /**
260    * When we do a "MUST_PASS_ONE" (a logical 'OR') of the above two filters
261    * we expect to get the same result as the 'prefix' only result.
262    * @throws Exception
263    */
264   public void testFilterListTwoFiltersMustPassOne() throws Exception {
265     byte[] r1 = Bytes.toBytes("Row1");
266     byte[] r11 = Bytes.toBytes("Row11");
267     byte[] r2 = Bytes.toBytes("Row2");
268   
269     FilterList flist = new FilterList(FilterList.Operator.MUST_PASS_ONE);
270     flist.addFilter(new PrefixFilter(r1));
271     flist.filterRowKey(r1, 0, r1.length);
272     assertEquals(flist.filterKeyValue(new KeyValue(r1,r1,r1)), ReturnCode.INCLUDE);
273     assertEquals(flist.filterKeyValue(new KeyValue(r11,r11,r11)), ReturnCode.INCLUDE);
274 
275     flist.reset();
276     flist.filterRowKey(r2, 0, r2.length);
277     assertEquals(flist.filterKeyValue(new KeyValue(r2,r2,r2)), ReturnCode.SKIP);
278   
279     flist = new FilterList(FilterList.Operator.MUST_PASS_ONE);
280     flist.addFilter(new AlwaysNextColFilter());
281     flist.addFilter(new PrefixFilter(r1));
282     flist.filterRowKey(r1, 0, r1.length);
283     assertEquals(flist.filterKeyValue(new KeyValue(r1,r1,r1)), ReturnCode.INCLUDE);
284     assertEquals(flist.filterKeyValue(new KeyValue(r11,r11,r11)), ReturnCode.INCLUDE);
285 
286     flist.reset();
287     flist.filterRowKey(r2, 0, r2.length);
288     assertEquals(flist.filterKeyValue(new KeyValue(r2,r2,r2)), ReturnCode.SKIP);
289   }
290 
291   /**
292    * When we do a "MUST_PASS_ONE" (a logical 'OR') of the two filters
293    * we expect to get the same result as the inclusive stop result.
294    * @throws Exception
295    */
296   public void testFilterListWithInclusiveStopFilteMustPassOne() throws Exception {
297     byte[] r1 = Bytes.toBytes("Row1");
298     byte[] r11 = Bytes.toBytes("Row11");
299     byte[] r2 = Bytes.toBytes("Row2");
300   
301     FilterList flist = new FilterList(FilterList.Operator.MUST_PASS_ONE);
302     flist.addFilter(new AlwaysNextColFilter());
303     flist.addFilter(new InclusiveStopFilter(r1));
304     flist.filterRowKey(r1, 0, r1.length);
305     assertEquals(flist.filterKeyValue(new KeyValue(r1,r1,r1)), ReturnCode.INCLUDE);
306     assertEquals(flist.filterKeyValue(new KeyValue(r11,r11,r11)), ReturnCode.INCLUDE);
307 
308     flist.reset();
309     flist.filterRowKey(r2, 0, r2.length);
310     assertEquals(flist.filterKeyValue(new KeyValue(r2,r2,r2)), ReturnCode.SKIP);
311   }
312 
313   public static class AlwaysNextColFilter extends FilterBase {
314     public AlwaysNextColFilter() {
315       super();
316     }
317     @Override
318     public ReturnCode filterKeyValue(Cell v) {
319       return ReturnCode.NEXT_COL;
320     }
321     public static AlwaysNextColFilter parseFrom(final byte [] pbBytes)
322         throws DeserializationException {
323       return new AlwaysNextColFilter();
324     }
325   }
326 
327   /**
328    * Test serialization
329    * @throws Exception
330    */
331   @Test
332   public void testSerialization() throws Exception {
333     List<Filter> filters = new ArrayList<Filter>();
334     filters.add(new PageFilter(MAX_PAGES));
335     filters.add(new WhileMatchFilter(new PrefixFilter(Bytes.toBytes("yyy"))));
336     Filter filterMPALL =
337       new FilterList(FilterList.Operator.MUST_PASS_ALL, filters);
338 
339     // Decompose filterMPALL to bytes.
340     byte[] buffer = filterMPALL.toByteArray();
341 
342     // Recompose filterMPALL.
343     FilterList newFilter = FilterList.parseFrom(buffer);
344 
345     // Run tests
346     mpOneTest(ProtobufUtil.toFilter(ProtobufUtil.toFilter(getFilterMPONE())));
347     mpAllTest(ProtobufUtil.toFilter(ProtobufUtil.toFilter(getMPALLFilter())));
348     orderingTest(ProtobufUtil.toFilter(ProtobufUtil.toFilter(getOrderingFilter())));
349   }
350 
351   /**
352    * Test filterKeyValue logic.
353    * @throws Exception
354    */
355   public void testFilterKeyValue() throws Exception {
356     Filter includeFilter = new FilterBase() {
357       @Override
358       public Filter.ReturnCode filterKeyValue(Cell v) {
359         return Filter.ReturnCode.INCLUDE;
360       }
361     };
362 
363     Filter alternateFilter = new FilterBase() {
364       boolean returnInclude = true;
365 
366       @Override
367       public Filter.ReturnCode filterKeyValue(Cell v) {
368         Filter.ReturnCode returnCode = returnInclude ? Filter.ReturnCode.INCLUDE :
369                                                        Filter.ReturnCode.SKIP;
370         returnInclude = !returnInclude;
371         return returnCode;
372       }
373     };
374 
375     Filter alternateIncludeFilter = new FilterBase() {
376       boolean returnIncludeOnly = false;
377 
378       @Override
379       public Filter.ReturnCode filterKeyValue(Cell v) {
380         Filter.ReturnCode returnCode = returnIncludeOnly ? Filter.ReturnCode.INCLUDE :
381                                                            Filter.ReturnCode.INCLUDE_AND_NEXT_COL;
382         returnIncludeOnly = !returnIncludeOnly;
383         return returnCode;
384       }
385     };
386 
387     // Check must pass one filter.
388     FilterList mpOnefilterList = new FilterList(Operator.MUST_PASS_ONE,
389         Arrays.asList(new Filter[] { includeFilter, alternateIncludeFilter, alternateFilter }));
390     // INCLUDE, INCLUDE, INCLUDE_AND_NEXT_COL.
391     assertEquals(Filter.ReturnCode.INCLUDE_AND_NEXT_COL, mpOnefilterList.filterKeyValue(null));
392     // INCLUDE, SKIP, INCLUDE. 
393     assertEquals(Filter.ReturnCode.INCLUDE, mpOnefilterList.filterKeyValue(null));
394 
395     // Check must pass all filter.
396     FilterList mpAllfilterList = new FilterList(Operator.MUST_PASS_ALL,
397         Arrays.asList(new Filter[] { includeFilter, alternateIncludeFilter, alternateFilter }));
398     // INCLUDE, INCLUDE, INCLUDE_AND_NEXT_COL.
399     assertEquals(Filter.ReturnCode.INCLUDE_AND_NEXT_COL, mpAllfilterList.filterKeyValue(null));
400     // INCLUDE, SKIP, INCLUDE. 
401     assertEquals(Filter.ReturnCode.SKIP, mpAllfilterList.filterKeyValue(null));
402   }
403 
404   /**
405    * Test pass-thru of hints.
406    */
407   @Test
408   public void testHintPassThru() throws Exception {
409 
410     final KeyValue minKeyValue = new KeyValue(Bytes.toBytes(0L), null, null);
411     final KeyValue maxKeyValue = new KeyValue(Bytes.toBytes(Long.MAX_VALUE),
412         null, null);
413 
414     Filter filterNoHint = new FilterBase() {
415       @Override
416       public byte [] toByteArray() {
417         return null;
418       }
419       
420       @Override
421       public ReturnCode filterKeyValue(Cell ignored) throws IOException {
422         return ReturnCode.INCLUDE;
423       }
424     };
425 
426     Filter filterMinHint = new FilterBase() {
427       @Override
428       public ReturnCode filterKeyValue(Cell ignored) {
429         return ReturnCode.SEEK_NEXT_USING_HINT;
430       }
431 
432       @Override
433       public Cell getNextCellHint(Cell currentKV) {
434         return minKeyValue;
435       }
436 
437       @Override
438       public byte [] toByteArray() {return null;}
439     };
440 
441     Filter filterMaxHint = new FilterBase() {
442       @Override
443       public ReturnCode filterKeyValue(Cell ignored) {
444         return ReturnCode.SEEK_NEXT_USING_HINT;
445       }
446 
447       @Override
448       public Cell getNextCellHint(Cell currentKV) {
449         return new KeyValue(Bytes.toBytes(Long.MAX_VALUE), null, null);
450       }
451 
452       @Override
453       public byte [] toByteArray() {return null;}
454     };
455 
456     // MUST PASS ONE
457 
458     // Should take the min if given two hints
459     FilterList filterList = new FilterList(Operator.MUST_PASS_ONE,
460         Arrays.asList(new Filter [] { filterMinHint, filterMaxHint } ));
461     assertEquals(0, KeyValue.COMPARATOR.compare(filterList.getNextKeyHint(null),
462         minKeyValue));
463 
464     // Should have no hint if any filter has no hint
465     filterList = new FilterList(Operator.MUST_PASS_ONE,
466         Arrays.asList(
467             new Filter [] { filterMinHint, filterMaxHint, filterNoHint } ));
468     assertNull(filterList.getNextKeyHint(null));
469     filterList = new FilterList(Operator.MUST_PASS_ONE,
470         Arrays.asList(new Filter [] { filterNoHint, filterMaxHint } ));
471     assertNull(filterList.getNextKeyHint(null));
472 
473     // Should give max hint if its the only one
474     filterList = new FilterList(Operator.MUST_PASS_ONE,
475         Arrays.asList(new Filter [] { filterMaxHint, filterMaxHint } ));
476     assertEquals(0, KeyValue.COMPARATOR.compare(filterList.getNextKeyHint(null),
477         maxKeyValue));
478 
479     // MUST PASS ALL
480 
481     // Should take the first hint
482     filterList = new FilterList(Operator.MUST_PASS_ALL,
483         Arrays.asList(new Filter [] { filterMinHint, filterMaxHint } ));
484     filterList.filterKeyValue(null);
485     assertEquals(0, KeyValue.COMPARATOR.compare(filterList.getNextKeyHint(null),
486         minKeyValue));
487 
488     filterList = new FilterList(Operator.MUST_PASS_ALL,
489         Arrays.asList(new Filter [] { filterMaxHint, filterMinHint } ));
490     filterList.filterKeyValue(null);
491     assertEquals(0, KeyValue.COMPARATOR.compare(filterList.getNextKeyHint(null),
492         maxKeyValue));
493 
494     // Should have first hint even if a filter has no hint
495     filterList = new FilterList(Operator.MUST_PASS_ALL,
496         Arrays.asList(
497             new Filter [] { filterNoHint, filterMinHint, filterMaxHint } ));
498     filterList.filterKeyValue(null);
499     assertEquals(0, KeyValue.COMPARATOR.compare(filterList.getNextKeyHint(null),
500         minKeyValue));
501     filterList = new FilterList(Operator.MUST_PASS_ALL,
502         Arrays.asList(new Filter [] { filterNoHint, filterMaxHint } ));
503     filterList.filterKeyValue(null);
504     assertEquals(0, KeyValue.COMPARATOR.compare(filterList.getNextKeyHint(null),
505         maxKeyValue));
506     filterList = new FilterList(Operator.MUST_PASS_ALL,
507         Arrays.asList(new Filter [] { filterNoHint, filterMinHint } ));
508     filterList.filterKeyValue(null);
509     assertEquals(0, KeyValue.COMPARATOR.compare(filterList.getNextKeyHint(null),
510         minKeyValue));
511   }
512 
513   /**
514    * Tests the behavior of transform() in a hierarchical filter.
515    *
516    * transform() only applies after a filterKeyValue() whose return-code includes the KeyValue.
517    * Lazy evaluation of AND
518    */
519   @Test
520   public void testTransformMPO() throws Exception {
521     // Apply the following filter:
522     //     (family=fam AND qualifier=qual1 AND KeyOnlyFilter)
523     //  OR (family=fam AND qualifier=qual2)
524     final FilterList flist = new FilterList(Operator.MUST_PASS_ONE, Lists.<Filter>newArrayList(
525         new FilterList(Operator.MUST_PASS_ALL, Lists.<Filter>newArrayList(
526             new FamilyFilter(CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("fam"))),
527             new QualifierFilter(CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("qual1"))),
528             new KeyOnlyFilter())),
529         new FilterList(Operator.MUST_PASS_ALL, Lists.<Filter>newArrayList(
530             new FamilyFilter(CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("fam"))),
531             new QualifierFilter(CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("qual2")))))));
532 
533     final KeyValue kvQual1 = new KeyValue(
534         Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("qual1"), Bytes.toBytes("value"));
535     final KeyValue kvQual2 = new KeyValue(
536         Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("qual2"), Bytes.toBytes("value"));
537     final KeyValue kvQual3 = new KeyValue(
538         Bytes.toBytes("row"), Bytes.toBytes("fam"), Bytes.toBytes("qual3"), Bytes.toBytes("value"));
539 
540     // Value for fam:qual1 should be stripped:
541     assertEquals(Filter.ReturnCode.INCLUDE, flist.filterKeyValue(kvQual1));
542     final KeyValue transformedQual1 = KeyValueUtil.ensureKeyValue(flist.transform(kvQual1));
543     assertEquals(0, transformedQual1.getValue().length);
544 
545     // Value for fam:qual2 should not be stripped:
546     assertEquals(Filter.ReturnCode.INCLUDE, flist.filterKeyValue(kvQual2));
547     final KeyValue transformedQual2 = KeyValueUtil.ensureKeyValue(flist.transform(kvQual2));
548     assertEquals("value", Bytes.toString(transformedQual2.getValue()));
549 
550     // Other keys should be skipped:
551     assertEquals(Filter.ReturnCode.SKIP, flist.filterKeyValue(kvQual3));
552   }
553 
554 }
555