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