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}