001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.types;
019
020import static org.junit.Assert.assertArrayEquals;
021import static org.junit.Assert.assertEquals;
022
023import java.lang.reflect.Constructor;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Comparator;
027import org.apache.hadoop.hbase.HBaseClassTestRule;
028import org.apache.hadoop.hbase.testclassification.MiscTests;
029import org.apache.hadoop.hbase.testclassification.SmallTests;
030import org.apache.hadoop.hbase.util.Bytes;
031import org.apache.hadoop.hbase.util.Order;
032import org.apache.hadoop.hbase.util.PositionedByteRange;
033import org.apache.hadoop.hbase.util.SimplePositionedMutableByteRange;
034import org.junit.ClassRule;
035import org.junit.Test;
036import org.junit.experimental.categories.Category;
037import org.junit.runner.RunWith;
038import org.junit.runners.Parameterized;
039import org.junit.runners.Parameterized.Parameters;
040
041/**
042 * This class both tests and demonstrates how to construct compound rowkeys
043 * from a POJO. The code under test is {@link Struct}.
044 * {@link SpecializedPojo1Type1} demonstrates how one might create their own
045 * custom data type extension for an application POJO.
046 */
047@RunWith(Parameterized.class)
048@Category({MiscTests.class, SmallTests.class})
049public class TestStruct {
050
051  @ClassRule
052  public static final HBaseClassTestRule CLASS_RULE =
053      HBaseClassTestRule.forClass(TestStruct.class);
054
055  @Parameterized.Parameter(value = 0)
056  public Struct generic;
057
058  @SuppressWarnings("rawtypes")
059  @Parameterized.Parameter(value = 1)
060  public DataType specialized;
061
062  @Parameterized.Parameter(value = 2)
063  public Object[][] constructorArgs;
064
065  @Parameters
066  public static Collection<Object[]> params() {
067    Object[][] pojo1Args = {
068      new Object[] { "foo", 5,   10.001 },
069      new Object[] { "foo", 100, 7.0    },
070      new Object[] { "foo", 100, 10.001 },
071      new Object[] { "bar", 5,   10.001 },
072      new Object[] { "bar", 100, 10.001 },
073      new Object[] { "baz", 5,   10.001 },
074    };
075
076    Object[][] pojo2Args = {
077      new Object[] { new byte[0], Bytes.toBytes("it"), "was", Bytes.toBytes("the") },
078      new Object[] { Bytes.toBytes("best"), new byte[0], "of", Bytes.toBytes("times,") },
079      new Object[] { Bytes.toBytes("it"), Bytes.toBytes("was"), "", Bytes.toBytes("the") },
080      new Object[] { Bytes.toBytes("worst"), Bytes.toBytes("of"), "times,", new byte[0] },
081      new Object[] { new byte[0], new byte[0], "", new byte[0] },
082    };
083
084    Object[][] params = new Object[][] {
085        { SpecializedPojo1Type1.GENERIC, new SpecializedPojo1Type1(), pojo1Args },
086        { SpecializedPojo2Type1.GENERIC, new SpecializedPojo2Type1(), pojo2Args },
087    };
088    return Arrays.asList(params);
089  }
090
091  static final Comparator<byte[]> NULL_SAFE_BYTES_COMPARATOR =
092      new Comparator<byte[]>() {
093        @Override
094        public int compare(byte[] o1, byte[] o2) {
095          if (o1 == o2) return 0;
096          if (null == o1) return -1;
097          if (null == o2) return 1;
098          return Bytes.compareTo(o1, o2);
099        }
100      };
101
102  /**
103   * A simple object to serialize.
104   */
105  private static class Pojo1 implements Comparable<Pojo1> {
106    final String stringFieldAsc;
107    final int intFieldAsc;
108    final double doubleFieldAsc;
109    final transient String str;
110
111    public Pojo1(Object... argv) {
112      stringFieldAsc = (String) argv[0];
113      intFieldAsc = (Integer) argv[1];
114      doubleFieldAsc = (Double) argv[2];
115      str = new StringBuilder()
116            .append("{ ")
117            .append(null == stringFieldAsc ? "" : "\"")
118            .append(stringFieldAsc)
119            .append(null == stringFieldAsc ? "" : "\"").append(", ")
120            .append(intFieldAsc).append(", ")
121            .append(doubleFieldAsc)
122            .append(" }")
123            .toString();
124    }
125
126    @Override
127    public String toString() {
128      return str;
129    }
130
131    @Override
132    public int compareTo(Pojo1 o) {
133      int cmp = stringFieldAsc.compareTo(o.stringFieldAsc);
134      if (cmp != 0) {
135        return cmp;
136      }
137      cmp = Integer.valueOf(intFieldAsc).compareTo(Integer.valueOf(o.intFieldAsc));
138      if (cmp != 0) {
139        return cmp;
140      }
141      return Double.compare(doubleFieldAsc, o.doubleFieldAsc);
142    }
143
144    @Override
145    public int hashCode() {
146      final int prime = 31;
147      int result = 1;
148      long temp;
149      temp = Double.doubleToLongBits(doubleFieldAsc);
150      result = prime * result + (int) (temp ^ (temp >>> 32));
151      result = prime * result + intFieldAsc;
152      result = prime * result + ((stringFieldAsc == null) ? 0 : stringFieldAsc.hashCode());
153      return result;
154    }
155
156    @Override
157    public boolean equals(Object obj) {
158      if (this == obj) {
159        return true;
160      }
161      if (obj == null) {
162        return false;
163      }
164      if (getClass() != obj.getClass()) {
165        return false;
166      }
167      Pojo1 other = (Pojo1) obj;
168      if (Double.doubleToLongBits(doubleFieldAsc) !=
169          Double.doubleToLongBits(other.doubleFieldAsc)) {
170        return false;
171      }
172      if (intFieldAsc != other.intFieldAsc) {
173        return false;
174      }
175      if (stringFieldAsc == null) {
176        if (other.stringFieldAsc != null) {
177          return false;
178        }
179      } else if (!stringFieldAsc.equals(other.stringFieldAsc)) {
180        return false;
181      }
182      return true;
183    }
184  }
185
186  /**
187   * A simple object to serialize.
188   */
189  private static class Pojo2 implements Comparable<Pojo2> {
190    final byte[] byteField1Asc;
191    final byte[] byteField2Dsc;
192    final String stringFieldDsc;
193    final byte[] byteField3Dsc;
194    final transient String str;
195
196    public Pojo2(Object... vals) {
197      byteField1Asc  = vals.length > 0 ? (byte[]) vals[0] : null;
198      byteField2Dsc  = vals.length > 1 ? (byte[]) vals[1] : null;
199      stringFieldDsc = vals.length > 2 ? (String) vals[2] : null;
200      byteField3Dsc  = vals.length > 3 ? (byte[]) vals[3] : null;
201      str = new StringBuilder()
202            .append("{ ")
203            .append(Bytes.toStringBinary(byteField1Asc)).append(", ")
204            .append(Bytes.toStringBinary(byteField2Dsc)).append(", ")
205            .append(null == stringFieldDsc ? "" : "\"")
206            .append(stringFieldDsc)
207            .append(null == stringFieldDsc ? "" : "\"").append(", ")
208            .append(Bytes.toStringBinary(byteField3Dsc))
209            .append(" }")
210            .toString();
211    }
212
213    @Override
214    public String toString() {
215      return str;
216    }
217
218    @Override
219    public int compareTo(Pojo2 o) {
220      int cmp = NULL_SAFE_BYTES_COMPARATOR.compare(byteField1Asc, o.byteField1Asc);
221      if (cmp != 0) {
222        return cmp;
223      }
224      cmp = -NULL_SAFE_BYTES_COMPARATOR.compare(byteField2Dsc, o.byteField2Dsc);
225      if (cmp != 0) {
226        return cmp;
227      }
228      if (null == stringFieldDsc) {
229        cmp = 1;
230      }
231      else if (null == o.stringFieldDsc) {
232        cmp = -1;
233      }
234      else if (stringFieldDsc.equals(o.stringFieldDsc)) {
235        cmp = 0;
236      }
237      else cmp = -stringFieldDsc.compareTo(o.stringFieldDsc);
238      if (cmp != 0) {
239        return cmp;
240      }
241      return -NULL_SAFE_BYTES_COMPARATOR.compare(byteField3Dsc, o.byteField3Dsc);
242    }
243
244    @Override
245    public int hashCode() {
246      final int prime = 31;
247      int result = 1;
248      result = prime * result + Arrays.hashCode(byteField1Asc);
249      result = prime * result + Arrays.hashCode(byteField2Dsc);
250      result = prime * result + Arrays.hashCode(byteField3Dsc);
251      result = prime * result + ((stringFieldDsc == null) ? 0 : stringFieldDsc.hashCode());
252      return result;
253    }
254
255    @Override
256    public boolean equals(Object obj) {
257      if (this == obj) {
258        return true;
259      }
260      if (obj == null) {
261        return false;
262      }
263      if (getClass() != obj.getClass()) {
264        return false;
265      }
266      Pojo2 other = (Pojo2) obj;
267      if (!Arrays.equals(byteField1Asc, other.byteField1Asc)) {
268        return false;
269      }
270      if (!Arrays.equals(byteField2Dsc, other.byteField2Dsc)) {
271        return false;
272      }
273      if (!Arrays.equals(byteField3Dsc, other.byteField3Dsc)) {
274        return false;
275      }
276      if (stringFieldDsc == null) {
277        if (other.stringFieldDsc != null) {
278          return false;
279        }
280      } else if (!stringFieldDsc.equals(other.stringFieldDsc)) {
281        return false;
282      }
283      return true;
284    }
285  }
286
287  /**
288   * A custom data type implementation specialized for {@link Pojo1}.
289   */
290  private static class SpecializedPojo1Type1 implements DataType<Pojo1> {
291
292    private static final RawStringTerminated stringField = new RawStringTerminated("/");
293    private static final RawInteger intField = new RawInteger();
294    private static final RawDouble doubleField = new RawDouble();
295
296    /**
297     * The {@link Struct} equivalent of this type.
298     */
299    public static Struct GENERIC =
300        new StructBuilder().add(stringField)
301                           .add(intField)
302                           .add(doubleField)
303                           .toStruct();
304
305    @Override
306    public boolean isOrderPreserving() { return true; }
307
308    @Override
309    public Order getOrder() { return null; }
310
311    @Override
312    public boolean isNullable() { return false; }
313
314    @Override
315    public boolean isSkippable() { return true; }
316
317    @Override
318    public int encodedLength(Pojo1 val) {
319      return
320          stringField.encodedLength(val.stringFieldAsc) +
321          intField.encodedLength(val.intFieldAsc) +
322          doubleField.encodedLength(val.doubleFieldAsc);
323    }
324
325    @Override
326    public Class<Pojo1> encodedClass() { return Pojo1.class; }
327
328    @Override
329    public int skip(PositionedByteRange src) {
330      int skipped = stringField.skip(src);
331      skipped += intField.skip(src);
332      skipped += doubleField.skip(src);
333      return skipped;
334    }
335
336    @Override
337    public Pojo1 decode(PositionedByteRange src) {
338      Object[] ret = new Object[3];
339      ret[0] = stringField.decode(src);
340      ret[1] = intField.decode(src);
341      ret[2] = doubleField.decode(src);
342      return new Pojo1(ret);
343    }
344
345    @Override
346    public int encode(PositionedByteRange dst, Pojo1 val) {
347      int written = stringField.encode(dst, val.stringFieldAsc);
348      written += intField.encode(dst, val.intFieldAsc);
349      written += doubleField.encode(dst, val.doubleFieldAsc);
350      return written;
351    }
352  }
353
354  /**
355   * A custom data type implementation specialized for {@link Pojo2}.
356   */
357  private static class SpecializedPojo2Type1 implements DataType<Pojo2> {
358
359    private static RawBytesTerminated byteField1 = new RawBytesTerminated("/");
360    private static RawBytesTerminated byteField2 =
361        new RawBytesTerminated(Order.DESCENDING, "/");
362    private static RawStringTerminated stringField =
363        new RawStringTerminated(Order.DESCENDING, new byte[] { 0x00 });
364    private static RawBytes byteField3 = new RawBytes(Order.DESCENDING);
365
366    /**
367     * The {@link Struct} equivalent of this type.
368     */
369    public static Struct GENERIC =
370        new StructBuilder().add(byteField1)
371                           .add(byteField2)
372                           .add(stringField)
373                           .add(byteField3)
374                           .toStruct();
375
376    @Override
377    public boolean isOrderPreserving() { return true; }
378
379    @Override
380    public Order getOrder() { return null; }
381
382    @Override
383    public boolean isNullable() { return false; }
384
385    @Override
386    public boolean isSkippable() { return true; }
387
388    @Override
389    public int encodedLength(Pojo2 val) {
390      return
391          byteField1.encodedLength(val.byteField1Asc) +
392          byteField2.encodedLength(val.byteField2Dsc) +
393          stringField.encodedLength(val.stringFieldDsc) +
394          byteField3.encodedLength(val.byteField3Dsc);
395    }
396
397    @Override
398    public Class<Pojo2> encodedClass() { return Pojo2.class; }
399
400    @Override
401    public int skip(PositionedByteRange src) {
402      int skipped = byteField1.skip(src);
403      skipped += byteField2.skip(src);
404      skipped += stringField.skip(src);
405      skipped += byteField3.skip(src);
406      return skipped;
407    }
408
409    @Override
410    public Pojo2 decode(PositionedByteRange src) {
411      Object[] ret = new Object[4];
412      ret[0] = byteField1.decode(src);
413      ret[1] = byteField2.decode(src);
414      ret[2] = stringField.decode(src);
415      ret[3] = byteField3.decode(src);
416      return new Pojo2(ret);
417    }
418
419    @Override
420    public int encode(PositionedByteRange dst, Pojo2 val) {
421      int written = byteField1.encode(dst, val.byteField1Asc);
422      written += byteField2.encode(dst, val.byteField2Dsc);
423      written += stringField.encode(dst, val.stringFieldDsc);
424      written += byteField3.encode(dst, val.byteField3Dsc);
425      return written;
426    }
427  }
428
429  @Test
430  @SuppressWarnings("unchecked")
431  public void testOrderPreservation() throws Exception {
432    Object[] vals = new Object[constructorArgs.length];
433    PositionedByteRange[] encodedGeneric = new PositionedByteRange[constructorArgs.length];
434    PositionedByteRange[] encodedSpecialized = new PositionedByteRange[constructorArgs.length];
435    Constructor<?> ctor = specialized.encodedClass().getConstructor(Object[].class);
436    for (int i = 0; i < vals.length; i++) {
437      vals[i] = ctor.newInstance(new Object[] { constructorArgs[i] });
438      encodedGeneric[i] = new SimplePositionedMutableByteRange(
439          generic.encodedLength(constructorArgs[i]));
440      encodedSpecialized[i] = new SimplePositionedMutableByteRange(
441          specialized.encodedLength(vals[i]));
442    }
443
444    // populate our arrays
445    for (int i = 0; i < vals.length; i++) {
446      generic.encode(encodedGeneric[i], constructorArgs[i]);
447      encodedGeneric[i].setPosition(0);
448      specialized.encode(encodedSpecialized[i], vals[i]);
449      encodedSpecialized[i].setPosition(0);
450      assertArrayEquals(encodedGeneric[i].getBytes(), encodedSpecialized[i].getBytes());
451    }
452
453    Arrays.sort(vals);
454    Arrays.sort(encodedGeneric);
455    Arrays.sort(encodedSpecialized);
456
457    for (int i = 0; i < vals.length; i++) {
458      assertEquals(
459        "Struct encoder does not preserve sort order at position " + i,
460        vals[i],
461        ctor.newInstance(new Object[] { generic.decode(encodedGeneric[i]) }));
462      assertEquals(
463        "Specialized encoder does not preserve sort order at position " + i,
464        vals[i], specialized.decode(encodedSpecialized[i]));
465    }
466  }
467}