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  @ClassRule
051  public static final HBaseClassTestRule CLASS_RULE =
052      HBaseClassTestRule.forClass(TestStruct.class);
053
054  @Parameterized.Parameter()
055  public Struct generic;
056
057  @SuppressWarnings("rawtypes")
058  @Parameterized.Parameter(value = 1)
059  public DataType specialized;
060
061  @Parameterized.Parameter(value = 2)
062  public Object[][] constructorArgs;
063
064  @Parameters
065  public static Collection<Object[]> params() {
066    Object[][] pojo1Args = {
067      new Object[] { "foo", 5,   10.001 },
068      new Object[] { "foo", 100, 7.0    },
069      new Object[] { "foo", 100, 10.001 },
070      new Object[] { "bar", 5,   10.001 },
071      new Object[] { "bar", 100, 10.001 },
072      new Object[] { "baz", 5,   10.001 },
073    };
074
075    Object[][] pojo2Args = {
076      new Object[] { new byte[0], Bytes.toBytes("it"), "was", Bytes.toBytes("the") },
077      new Object[] { Bytes.toBytes("best"), new byte[0], "of", Bytes.toBytes("times,") },
078      new Object[] { Bytes.toBytes("it"), Bytes.toBytes("was"), "", Bytes.toBytes("the") },
079      new Object[] { Bytes.toBytes("worst"), Bytes.toBytes("of"), "times,", new byte[0] },
080      new Object[] { new byte[0], new byte[0], "", new byte[0] },
081    };
082
083    Object[][] params = new Object[][] {
084        { SpecializedPojo1Type1.GENERIC, new SpecializedPojo1Type1(), pojo1Args },
085        { SpecializedPojo2Type1.GENERIC, new SpecializedPojo2Type1(), pojo2Args },
086    };
087    return Arrays.asList(params);
088  }
089
090  static final Comparator<byte[]> NULL_SAFE_BYTES_COMPARATOR = (o1, o2) -> {
091    if (o1 == o2) {
092      return 0;
093    }
094
095    if (null == o1) {
096      return -1;
097    }
098
099    if (null == o2) {
100      return 1;
101    }
102
103    return Bytes.compareTo(o1, o2);
104  };
105
106  /**
107   * A simple object to serialize.
108   */
109  private static class Pojo1 implements Comparable<Pojo1> {
110    final String stringFieldAsc;
111    final int intFieldAsc;
112    final double doubleFieldAsc;
113    final transient String str;
114
115    public Pojo1(Object... argv) {
116      stringFieldAsc = (String) argv[0];
117      intFieldAsc = (Integer) argv[1];
118      doubleFieldAsc = (Double) argv[2];
119      str = new StringBuilder()
120            .append("{ ")
121            .append(null == stringFieldAsc ? "" : "\"")
122            .append(stringFieldAsc)
123            .append(null == stringFieldAsc ? "" : "\"").append(", ")
124            .append(intFieldAsc).append(", ")
125            .append(doubleFieldAsc)
126            .append(" }")
127            .toString();
128    }
129
130    @Override
131    public String toString() {
132      return str;
133    }
134
135    @Override
136    public int compareTo(Pojo1 o) {
137      int cmp = stringFieldAsc.compareTo(o.stringFieldAsc);
138      if (cmp != 0) {
139        return cmp;
140      }
141      cmp = Integer.compare(intFieldAsc, o.intFieldAsc);
142      if (cmp != 0) {
143        return cmp;
144      }
145      return Double.compare(doubleFieldAsc, o.doubleFieldAsc);
146    }
147
148    @Override
149    public int hashCode() {
150      final int prime = 31;
151      int result = 1;
152      long temp;
153      temp = Double.doubleToLongBits(doubleFieldAsc);
154      result = prime * result + (int) (temp ^ (temp >>> 32));
155      result = prime * result + intFieldAsc;
156      result = prime * result + ((stringFieldAsc == null) ? 0 : stringFieldAsc.hashCode());
157      return result;
158    }
159
160    @Override
161    public boolean equals(Object obj) {
162      if (this == obj) {
163        return true;
164      }
165      if (obj == null) {
166        return false;
167      }
168      if (getClass() != obj.getClass()) {
169        return false;
170      }
171      Pojo1 other = (Pojo1) obj;
172      if (Double.doubleToLongBits(doubleFieldAsc) !=
173          Double.doubleToLongBits(other.doubleFieldAsc)) {
174        return false;
175      }
176      if (intFieldAsc != other.intFieldAsc) {
177        return false;
178      }
179      if (stringFieldAsc == null) {
180        return other.stringFieldAsc == null;
181      } else {
182        return stringFieldAsc.equals(other.stringFieldAsc);
183      }
184    }
185  }
186
187  /**
188   * A simple object to serialize.
189   */
190  private static class Pojo2 implements Comparable<Pojo2> {
191    final byte[] byteField1Asc;
192    final byte[] byteField2Dsc;
193    final String stringFieldDsc;
194    final byte[] byteField3Dsc;
195    final transient String str;
196
197    public Pojo2(Object... vals) {
198      byteField1Asc  = vals.length > 0 ? (byte[]) vals[0] : null;
199      byteField2Dsc  = vals.length > 1 ? (byte[]) vals[1] : null;
200      stringFieldDsc = vals.length > 2 ? (String) vals[2] : null;
201      byteField3Dsc  = vals.length > 3 ? (byte[]) vals[3] : null;
202      str = new StringBuilder()
203            .append("{ ")
204            .append(Bytes.toStringBinary(byteField1Asc)).append(", ")
205            .append(Bytes.toStringBinary(byteField2Dsc)).append(", ")
206            .append(null == stringFieldDsc ? "" : "\"")
207            .append(stringFieldDsc)
208            .append(null == stringFieldDsc ? "" : "\"").append(", ")
209            .append(Bytes.toStringBinary(byteField3Dsc))
210            .append(" }")
211            .toString();
212    }
213
214    @Override
215    public String toString() {
216      return str;
217    }
218
219    @Override
220    public int compareTo(Pojo2 o) {
221      int cmp = NULL_SAFE_BYTES_COMPARATOR.compare(byteField1Asc, o.byteField1Asc);
222      if (cmp != 0) {
223        return cmp;
224      }
225      cmp = -NULL_SAFE_BYTES_COMPARATOR.compare(byteField2Dsc, o.byteField2Dsc);
226      if (cmp != 0) {
227        return cmp;
228      }
229
230      if (null == stringFieldDsc) {
231        cmp = 1;
232      } else if (null == o.stringFieldDsc) {
233        cmp = -1;
234      } else if (stringFieldDsc.equals(o.stringFieldDsc)) {
235        cmp = 0;
236      } else {
237        cmp = -stringFieldDsc.compareTo(o.stringFieldDsc);
238      }
239
240      if (cmp != 0) {
241        return cmp;
242      }
243      return -NULL_SAFE_BYTES_COMPARATOR.compare(byteField3Dsc, o.byteField3Dsc);
244    }
245
246    @Override
247    public int hashCode() {
248      final int prime = 31;
249      int result = 1;
250      result = prime * result + Arrays.hashCode(byteField1Asc);
251      result = prime * result + Arrays.hashCode(byteField2Dsc);
252      result = prime * result + Arrays.hashCode(byteField3Dsc);
253      result = prime * result + ((stringFieldDsc == null) ? 0 : stringFieldDsc.hashCode());
254      return result;
255    }
256
257    @Override
258    public boolean equals(Object obj) {
259      if (this == obj) {
260        return true;
261      }
262      if (obj == null) {
263        return false;
264      }
265      if (getClass() != obj.getClass()) {
266        return false;
267      }
268      Pojo2 other = (Pojo2) obj;
269      if (!Arrays.equals(byteField1Asc, other.byteField1Asc)) {
270        return false;
271      }
272      if (!Arrays.equals(byteField2Dsc, other.byteField2Dsc)) {
273        return false;
274      }
275      if (!Arrays.equals(byteField3Dsc, other.byteField3Dsc)) {
276        return false;
277      }
278      if (stringFieldDsc == null) {
279        return other.stringFieldDsc == null;
280      } else {
281        return stringFieldDsc.equals(other.stringFieldDsc);
282      }
283    }
284  }
285
286  /**
287   * A custom data type implementation specialized for {@link Pojo1}.
288   */
289  private static class SpecializedPojo1Type1 implements DataType<Pojo1> {
290    private static final RawStringTerminated stringField = new RawStringTerminated("/");
291    private static final RawInteger intField = new RawInteger();
292    private static final RawDouble doubleField = new RawDouble();
293
294    /**
295     * The {@link Struct} equivalent of this type.
296     */
297    public static Struct GENERIC = new StructBuilder().add(stringField).add(intField)
298      .add(doubleField).toStruct();
299
300    @Override
301    public boolean isOrderPreserving() {
302      return true;
303    }
304
305    @Override
306    public Order getOrder() {
307      return null;
308    }
309
310    @Override
311    public boolean isNullable() {
312      return false;
313    }
314
315    @Override
316    public boolean isSkippable() {
317      return true;
318    }
319
320    @Override
321    public int encodedLength(Pojo1 val) {
322      return stringField.encodedLength(val.stringFieldAsc) +
323          intField.encodedLength(val.intFieldAsc) +
324          doubleField.encodedLength(val.doubleFieldAsc);
325    }
326
327    @Override
328    public Class<Pojo1> encodedClass() {
329      return Pojo1.class;
330    }
331
332    @Override
333    public int skip(PositionedByteRange src) {
334      int skipped = stringField.skip(src);
335      skipped += intField.skip(src);
336      skipped += doubleField.skip(src);
337      return skipped;
338    }
339
340    @Override
341    public Pojo1 decode(PositionedByteRange src) {
342      Object[] ret = new Object[3];
343      ret[0] = stringField.decode(src);
344      ret[1] = intField.decode(src);
345      ret[2] = doubleField.decode(src);
346      return new Pojo1(ret);
347    }
348
349    @Override
350    public int encode(PositionedByteRange dst, Pojo1 val) {
351      int written = stringField.encode(dst, val.stringFieldAsc);
352      written += intField.encode(dst, val.intFieldAsc);
353      written += doubleField.encode(dst, val.doubleFieldAsc);
354      return written;
355    }
356  }
357
358  /**
359   * A custom data type implementation specialized for {@link Pojo2}.
360   */
361  private static class SpecializedPojo2Type1 implements DataType<Pojo2> {
362    private static RawBytesTerminated byteField1 = new RawBytesTerminated("/");
363    private static RawBytesTerminated byteField2 =
364        new RawBytesTerminated(Order.DESCENDING, "/");
365    private static RawStringTerminated stringField =
366        new RawStringTerminated(Order.DESCENDING, new byte[] { 0x00 });
367    private static RawBytes byteField3 = new RawBytes(Order.DESCENDING);
368
369    /**
370     * The {@link Struct} equivalent of this type.
371     */
372    public static Struct GENERIC = new StructBuilder().add(byteField1).add(byteField2)
373      .add(stringField).add(byteField3).toStruct();
374
375    @Override
376    public boolean isOrderPreserving() {
377      return true;
378    }
379
380    @Override
381    public Order getOrder() {
382      return null;
383    }
384
385    @Override
386    public boolean isNullable() {
387      return false;
388    }
389
390    @Override
391    public boolean isSkippable() {
392      return true;
393    }
394
395    @Override
396    public int encodedLength(Pojo2 val) {
397      return byteField1.encodedLength(val.byteField1Asc) +
398          byteField2.encodedLength(val.byteField2Dsc) +
399          stringField.encodedLength(val.stringFieldDsc) +
400          byteField3.encodedLength(val.byteField3Dsc);
401    }
402
403    @Override
404    public Class<Pojo2> encodedClass() {
405      return Pojo2.class;
406    }
407
408    @Override
409    public int skip(PositionedByteRange src) {
410      int skipped = byteField1.skip(src);
411      skipped += byteField2.skip(src);
412      skipped += stringField.skip(src);
413      skipped += byteField3.skip(src);
414      return skipped;
415    }
416
417    @Override
418    public Pojo2 decode(PositionedByteRange src) {
419      Object[] ret = new Object[4];
420      ret[0] = byteField1.decode(src);
421      ret[1] = byteField2.decode(src);
422      ret[2] = stringField.decode(src);
423      ret[3] = byteField3.decode(src);
424      return new Pojo2(ret);
425    }
426
427    @Override
428    public int encode(PositionedByteRange dst, Pojo2 val) {
429      int written = byteField1.encode(dst, val.byteField1Asc);
430      written += byteField2.encode(dst, val.byteField2Dsc);
431      written += stringField.encode(dst, val.stringFieldDsc);
432      written += byteField3.encode(dst, val.byteField3Dsc);
433      return written;
434    }
435  }
436
437  @Test
438  @SuppressWarnings("unchecked")
439  public void testOrderPreservation() throws Exception {
440    Object[] vals = new Object[constructorArgs.length];
441    PositionedByteRange[] encodedGeneric = new PositionedByteRange[constructorArgs.length];
442    PositionedByteRange[] encodedSpecialized = new PositionedByteRange[constructorArgs.length];
443    Constructor<?> ctor = specialized.encodedClass().getConstructor(Object[].class);
444    for (int i = 0; i < vals.length; i++) {
445      vals[i] = ctor.newInstance(new Object[] { constructorArgs[i] });
446      encodedGeneric[i] = new SimplePositionedMutableByteRange(
447          generic.encodedLength(constructorArgs[i]));
448      encodedSpecialized[i] = new SimplePositionedMutableByteRange(
449          specialized.encodedLength(vals[i]));
450    }
451
452    // populate our arrays
453    for (int i = 0; i < vals.length; i++) {
454      generic.encode(encodedGeneric[i], constructorArgs[i]);
455      encodedGeneric[i].setPosition(0);
456      specialized.encode(encodedSpecialized[i], vals[i]);
457      encodedSpecialized[i].setPosition(0);
458      assertArrayEquals(encodedGeneric[i].getBytes(), encodedSpecialized[i].getBytes());
459    }
460
461    Arrays.sort(vals);
462    Arrays.sort(encodedGeneric);
463    Arrays.sort(encodedSpecialized);
464
465    for (int i = 0; i < vals.length; i++) {
466      assertEquals(
467        "Struct encoder does not preserve sort order at position " + i,
468        vals[i],
469        ctor.newInstance(new Object[] { generic.decode(encodedGeneric[i]) }));
470      assertEquals(
471        "Specialized encoder does not preserve sort order at position " + i,
472        vals[i], specialized.decode(encodedSpecialized[i]));
473    }
474  }
475}