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