/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hbase.types; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import org.apache.hadoop.hbase.testclassification.SmallTests; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Order; import org.apache.hadoop.hbase.util.PositionedByteRange; import org.apache.hadoop.hbase.util.SimplePositionedMutableByteRange; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; /** * This class both tests and demonstrates how to construct compound rowkeys * from a POJO. The code under test is {@link Struct}. * {@link SpecializedPojo1Type1} demonstrates how one might create their own * custom data type extension for an application POJO. */ @RunWith(Parameterized.class) @Category(SmallTests.class) public class TestStruct { private Struct generic; @SuppressWarnings("rawtypes") private DataType specialized; private Object[][] constructorArgs; public TestStruct(Struct generic, @SuppressWarnings("rawtypes") DataType specialized, Object[][] constructorArgs) { this.generic = generic; this.specialized = specialized; this.constructorArgs = constructorArgs; } @Parameters public static Collection<Object[]> params() { Object[][] pojo1Args = { new Object[] { "foo", 5, 10.001 }, new Object[] { "foo", 100, 7.0 }, new Object[] { "foo", 100, 10.001 }, new Object[] { "bar", 5, 10.001 }, new Object[] { "bar", 100, 10.001 }, new Object[] { "baz", 5, 10.001 }, }; Object[][] pojo2Args = { new Object[] { new byte[0], "it".getBytes(), "was", "the".getBytes() }, new Object[] { "best".getBytes(), new byte[0], "of", "times,".getBytes() }, new Object[] { "it".getBytes(), "was".getBytes(), "", "the".getBytes() }, new Object[] { "worst".getBytes(), "of".getBytes(), "times,", new byte[0] }, new Object[] { new byte[0], new byte[0], "", new byte[0] }, }; Object[][] params = new Object[][] { { SpecializedPojo1Type1.GENERIC, new SpecializedPojo1Type1(), pojo1Args }, { SpecializedPojo2Type1.GENERIC, new SpecializedPojo2Type1(), pojo2Args }, }; return Arrays.asList(params); } static final Comparator<byte[]> NULL_SAFE_BYTES_COMPARATOR = new Comparator<byte[]>() { @Override public int compare(byte[] o1, byte[] o2) { if (o1 == o2) return 0; if (null == o1) return -1; if (null == o2) return 1; return Bytes.compareTo(o1, o2); } }; /** * A simple object to serialize. */ private static class Pojo1 implements Comparable<Pojo1> { final String stringFieldAsc; final int intFieldAsc; final double doubleFieldAsc; final transient String str; public Pojo1(Object... argv) { stringFieldAsc = (String) argv[0]; intFieldAsc = (Integer) argv[1]; doubleFieldAsc = (Double) argv[2]; str = new StringBuilder() .append("{ ") .append(null == stringFieldAsc ? "" : "\"") .append(stringFieldAsc) .append(null == stringFieldAsc ? "" : "\"").append(", ") .append(intFieldAsc).append(", ") .append(doubleFieldAsc) .append(" }") .toString(); } @Override public String toString() { return str; } @Override public int compareTo(Pojo1 o) { int cmp = stringFieldAsc.compareTo(o.stringFieldAsc); if (cmp != 0) return cmp; cmp = Integer.valueOf(intFieldAsc).compareTo(Integer.valueOf(o.intFieldAsc)); if (cmp != 0) return cmp; return Double.compare(doubleFieldAsc, o.doubleFieldAsc); } @Override public boolean equals(Object o) { if (this == o) return true; if (null == o) return false; if (!(o instanceof Pojo1)) return false; Pojo1 that = (Pojo1) o; return 0 == this.compareTo(that); } } /** * A simple object to serialize. */ private static class Pojo2 implements Comparable<Pojo2> { final byte[] byteField1Asc; final byte[] byteField2Dsc; final String stringFieldDsc; final byte[] byteField3Dsc; final transient String str; public Pojo2(Object... vals) { byteField1Asc = vals.length > 0 ? (byte[]) vals[0] : null; byteField2Dsc = vals.length > 1 ? (byte[]) vals[1] : null; stringFieldDsc = vals.length > 2 ? (String) vals[2] : null; byteField3Dsc = vals.length > 3 ? (byte[]) vals[3] : null; str = new StringBuilder() .append("{ ") .append(Bytes.toStringBinary(byteField1Asc)).append(", ") .append(Bytes.toStringBinary(byteField2Dsc)).append(", ") .append(null == stringFieldDsc ? "" : "\"") .append(stringFieldDsc) .append(null == stringFieldDsc ? "" : "\"").append(", ") .append(Bytes.toStringBinary(byteField3Dsc)) .append(" }") .toString(); } @Override public String toString() { return str; } @Override public int compareTo(Pojo2 o) { int cmp = NULL_SAFE_BYTES_COMPARATOR.compare(byteField1Asc, o.byteField1Asc); if (cmp != 0) return cmp; cmp = -NULL_SAFE_BYTES_COMPARATOR.compare(byteField2Dsc, o.byteField2Dsc); if (cmp != 0) return cmp; if (stringFieldDsc == o.stringFieldDsc) cmp = 0; else if (null == stringFieldDsc) cmp = 1; else if (null == o.stringFieldDsc) cmp = -1; else cmp = -stringFieldDsc.compareTo(o.stringFieldDsc); if (cmp != 0) return cmp; return -NULL_SAFE_BYTES_COMPARATOR.compare(byteField3Dsc, o.byteField3Dsc); } @Override public boolean equals(Object o) { if (this == o) return true; if (null == o) return false; if (!(o instanceof Pojo2)) return false; Pojo2 that = (Pojo2) o; return 0 == this.compareTo(that); } } /** * A custom data type implementation specialized for {@link Pojo1}. */ private static class SpecializedPojo1Type1 implements DataType<Pojo1> { private static final RawStringTerminated stringField = new RawStringTerminated("/"); private static final RawInteger intField = new RawInteger(); private static final RawDouble doubleField = new RawDouble(); /** * The {@link Struct} equivalent of this type. */ public static Struct GENERIC = new StructBuilder().add(stringField) .add(intField) .add(doubleField) .toStruct(); @Override public boolean isOrderPreserving() { return true; } @Override public Order getOrder() { return null; } @Override public boolean isNullable() { return false; } @Override public boolean isSkippable() { return true; } @Override public int encodedLength(Pojo1 val) { return stringField.encodedLength(val.stringFieldAsc) + intField.encodedLength(val.intFieldAsc) + doubleField.encodedLength(val.doubleFieldAsc); } @Override public Class<Pojo1> encodedClass() { return Pojo1.class; } @Override public int skip(PositionedByteRange src) { int skipped = stringField.skip(src); skipped += intField.skip(src); skipped += doubleField.skip(src); return skipped; } @Override public Pojo1 decode(PositionedByteRange src) { Object[] ret = new Object[3]; ret[0] = stringField.decode(src); ret[1] = intField.decode(src); ret[2] = doubleField.decode(src); return new Pojo1(ret); } @Override public int encode(PositionedByteRange dst, Pojo1 val) { int written = stringField.encode(dst, val.stringFieldAsc); written += intField.encode(dst, val.intFieldAsc); written += doubleField.encode(dst, val.doubleFieldAsc); return written; } } /** * A custom data type implementation specialized for {@link Pojo2}. */ private static class SpecializedPojo2Type1 implements DataType<Pojo2> { private static RawBytesTerminated byteField1 = new RawBytesTerminated("/"); private static RawBytesTerminated byteField2 = new RawBytesTerminated(Order.DESCENDING, "/"); private static RawStringTerminated stringField = new RawStringTerminated(Order.DESCENDING, new byte[] { 0x00 }); private static RawBytes byteField3 = RawBytes.DESCENDING; /** * The {@link Struct} equivalent of this type. */ public static Struct GENERIC = new StructBuilder().add(byteField1) .add(byteField2) .add(stringField) .add(byteField3) .toStruct(); @Override public boolean isOrderPreserving() { return true; } @Override public Order getOrder() { return null; } @Override public boolean isNullable() { return false; } @Override public boolean isSkippable() { return true; } @Override public int encodedLength(Pojo2 val) { return byteField1.encodedLength(val.byteField1Asc) + byteField2.encodedLength(val.byteField2Dsc) + stringField.encodedLength(val.stringFieldDsc) + byteField3.encodedLength(val.byteField3Dsc); } @Override public Class<Pojo2> encodedClass() { return Pojo2.class; } @Override public int skip(PositionedByteRange src) { int skipped = byteField1.skip(src); skipped += byteField2.skip(src); skipped += stringField.skip(src); skipped += byteField3.skip(src); return skipped; } @Override public Pojo2 decode(PositionedByteRange src) { Object[] ret = new Object[4]; ret[0] = byteField1.decode(src); ret[1] = byteField2.decode(src); ret[2] = stringField.decode(src); ret[3] = byteField3.decode(src); return new Pojo2(ret); } @Override public int encode(PositionedByteRange dst, Pojo2 val) { int written = byteField1.encode(dst, val.byteField1Asc); written += byteField2.encode(dst, val.byteField2Dsc); written += stringField.encode(dst, val.stringFieldDsc); written += byteField3.encode(dst, val.byteField3Dsc); return written; } } @Test @SuppressWarnings("unchecked") public void testOrderPreservation() throws Exception { Object[] vals = new Object[constructorArgs.length]; PositionedByteRange[] encodedGeneric = new PositionedByteRange[constructorArgs.length]; PositionedByteRange[] encodedSpecialized = new PositionedByteRange[constructorArgs.length]; Constructor<?> ctor = specialized.encodedClass().getConstructor(Object[].class); for (int i = 0; i < vals.length; i++) { vals[i] = ctor.newInstance(new Object[] { constructorArgs[i] }); encodedGeneric[i] = new SimplePositionedMutableByteRange( generic.encodedLength(constructorArgs[i])); encodedSpecialized[i] = new SimplePositionedMutableByteRange( specialized.encodedLength(vals[i])); } // populate our arrays for (int i = 0; i < vals.length; i++) { generic.encode(encodedGeneric[i], constructorArgs[i]); encodedGeneric[i].setPosition(0); specialized.encode(encodedSpecialized[i], vals[i]); encodedSpecialized[i].setPosition(0); assertArrayEquals(encodedGeneric[i].getBytes(), encodedSpecialized[i].getBytes()); } Arrays.sort(vals); Arrays.sort(encodedGeneric); Arrays.sort(encodedSpecialized); for (int i = 0; i < vals.length; i++) { assertEquals( "Struct encoder does not preserve sort order at position " + i, vals[i], ctor.newInstance(new Object[] { generic.decode(encodedGeneric[i]) })); assertEquals( "Specialized encoder does not preserve sort order at position " + i, vals[i], specialized.decode(encodedSpecialized[i])); } } }