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