| /* |
| * 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.druid.query.cache; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.Lists; |
| import com.google.common.primitives.Ints; |
| import com.google.common.primitives.Longs; |
| import com.google.common.primitives.UnsignedBytes; |
| import org.apache.druid.guice.annotations.PublicApi; |
| import org.apache.druid.java.util.common.Cacheable; |
| import org.apache.druid.java.util.common.StringUtils; |
| |
| import javax.annotation.Nullable; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * CacheKeyBuilder is a tool for easily generating cache keys of {@link Cacheable} objects. |
| * |
| * The layout of the serialized cache key is like below. |
| * |
| * +--------------------------------------------------------+ |
| * | ID (1 byte) | |
| * | type key (1 byte) | serialized value (variable length) | |
| * | type key (1 byte) | serialized value (variable length) | |
| * | ... | |
| * +--------------------------------------------------------+ |
| * |
| */ |
| @PublicApi |
| public class CacheKeyBuilder |
| { |
| static final byte BYTE_KEY = 0; |
| static final byte BYTE_ARRAY_KEY = 1; |
| static final byte BOOLEAN_KEY = 2; |
| static final byte INT_KEY = 3; |
| static final byte FLOAT_KEY = 4; |
| static final byte FLOAT_ARRAY_KEY = 5; |
| static final byte DOUBLE_KEY = 6; |
| static final byte STRING_KEY = 7; |
| static final byte STRING_LIST_KEY = 8; |
| static final byte CACHEABLE_KEY = 9; |
| static final byte CACHEABLE_LIST_KEY = 10; |
| static final byte DOUBLE_ARRAY_KEY = 11; |
| static final byte LONG_KEY = 12; |
| |
| static final byte[] STRING_SEPARATOR = new byte[]{(byte) 0xFF}; |
| static final byte[] EMPTY_BYTES = StringUtils.EMPTY_BYTES; |
| |
| private static class Item |
| { |
| private final byte typeKey; |
| private final byte[] item; |
| |
| Item(byte typeKey, byte[] item) |
| { |
| this.typeKey = typeKey; |
| this.item = item; |
| } |
| |
| int byteSize() |
| { |
| return 1 + item.length; |
| } |
| } |
| |
| private static byte[] floatArrayToByteArray(float[] input) |
| { |
| final ByteBuffer buffer = ByteBuffer.allocate(Float.BYTES * input.length); |
| buffer.asFloatBuffer().put(input); |
| return buffer.array(); |
| } |
| |
| private static byte[] doubleArrayToByteArray(double[] input) |
| { |
| final ByteBuffer buffer = ByteBuffer.allocate(Double.BYTES * input.length); |
| buffer.asDoubleBuffer().put(input); |
| return buffer.array(); |
| } |
| |
| private static byte[] cacheableToByteArray(@Nullable Cacheable cacheable) |
| { |
| if (cacheable == null) { |
| return EMPTY_BYTES; |
| } else { |
| final byte[] key = cacheable.getCacheKey(); |
| Preconditions.checkArgument(!Arrays.equals(key, EMPTY_BYTES), "cache key is equal to the empty key"); |
| return key; |
| } |
| } |
| |
| private static byte[] stringCollectionToByteArray(Collection<String> input, boolean preserveOrder) |
| { |
| return collectionToByteArray( |
| input, |
| new Function<String, byte[]>() |
| { |
| @Override |
| public byte[] apply(@Nullable String input) |
| { |
| return StringUtils.toUtf8WithNullToEmpty(input); |
| } |
| }, |
| STRING_SEPARATOR, |
| preserveOrder |
| ); |
| } |
| |
| private static byte[] cacheableCollectionToByteArray(Collection<? extends Cacheable> input, boolean preserveOrder) |
| { |
| return collectionToByteArray( |
| input, |
| new Function<Cacheable, byte[]>() |
| { |
| @Override |
| public byte[] apply(@Nullable Cacheable input) |
| { |
| return input == null ? EMPTY_BYTES : input.getCacheKey(); |
| } |
| }, |
| EMPTY_BYTES, |
| preserveOrder |
| ); |
| } |
| |
| private static <T> byte[] collectionToByteArray( |
| Collection<? extends T> collection, |
| Function<T, byte[]> serializeFunction, |
| byte[] separator, |
| boolean preserveOrder |
| ) |
| { |
| if (collection.size() > 0) { |
| List<byte[]> byteArrayList = Lists.newArrayListWithCapacity(collection.size()); |
| int totalByteLength = 0; |
| for (T eachItem : collection) { |
| final byte[] byteArray = serializeFunction.apply(eachItem); |
| totalByteLength += byteArray.length; |
| byteArrayList.add(byteArray); |
| } |
| |
| if (!preserveOrder) { |
| // Sort the byte array list to guarantee that collections of same items but in different orders make the same result |
| Collections.sort(byteArrayList, UnsignedBytes.lexicographicalComparator()); |
| } |
| |
| final Iterator<byte[]> iterator = byteArrayList.iterator(); |
| final int bufSize = Integer.BYTES + separator.length * (byteArrayList.size() - 1) + totalByteLength; |
| final ByteBuffer buffer = ByteBuffer.allocate(bufSize) |
| .putInt(byteArrayList.size()) |
| .put(iterator.next()); |
| |
| while (iterator.hasNext()) { |
| buffer.put(separator).put(iterator.next()); |
| } |
| |
| return buffer.array(); |
| } else { |
| return EMPTY_BYTES; |
| } |
| } |
| |
| private final List<Item> items = new ArrayList<>(); |
| private final byte id; |
| private int size; |
| |
| public CacheKeyBuilder(byte id) |
| { |
| this.id = id; |
| this.size = 1; |
| } |
| |
| public CacheKeyBuilder appendByte(byte input) |
| { |
| appendItem(BYTE_KEY, new byte[]{input}); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendByteArray(byte[] input) |
| { |
| appendItem(BYTE_ARRAY_KEY, input); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendString(@Nullable String input) |
| { |
| appendItem(STRING_KEY, StringUtils.toUtf8WithNullToEmpty(input)); |
| return this; |
| } |
| |
| /** |
| * Add a collection of strings to the cache key. |
| * Strings in the collection are concatenated with a separator of '0xFF', |
| * and they appear in the cache key in their input order. |
| * |
| * @param input a collection of strings to be included in the cache key |
| * @return this instance |
| */ |
| public CacheKeyBuilder appendStrings(Collection<String> input) |
| { |
| appendItem(STRING_LIST_KEY, stringCollectionToByteArray(input, true)); |
| return this; |
| } |
| |
| /** |
| * Add a collection of strings to the cache key. |
| * Strings in the collection are sorted by their byte representation and |
| * concatenated with a separator of '0xFF'. |
| * |
| * @param input a collection of strings to be included in the cache key |
| * @return this instance |
| */ |
| public CacheKeyBuilder appendStringsIgnoringOrder(Collection<String> input) |
| { |
| appendItem(STRING_LIST_KEY, stringCollectionToByteArray(input, false)); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendBoolean(boolean input) |
| { |
| appendItem(BOOLEAN_KEY, new byte[]{(byte) (input ? 1 : 0)}); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendInt(int input) |
| { |
| appendItem(INT_KEY, Ints.toByteArray(input)); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendLong(long input) |
| { |
| appendItem(LONG_KEY, Longs.toByteArray(input)); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendFloat(float input) |
| { |
| appendItem(FLOAT_KEY, ByteBuffer.allocate(Float.BYTES).putFloat(input).array()); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendDouble(double input) |
| { |
| appendItem(DOUBLE_KEY, ByteBuffer.allocate(Double.BYTES).putDouble(input).array()); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendDoubleArray(double[] input) |
| { |
| appendItem(DOUBLE_ARRAY_KEY, doubleArrayToByteArray(input)); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendFloatArray(float[] input) |
| { |
| appendItem(FLOAT_ARRAY_KEY, floatArrayToByteArray(input)); |
| return this; |
| } |
| |
| public CacheKeyBuilder appendCacheable(@Nullable Cacheable input) |
| { |
| appendItem(CACHEABLE_KEY, cacheableToByteArray(input)); |
| return this; |
| } |
| |
| /** |
| * Add a collection of Cacheables to the cache key. |
| * Cacheables in the collection are concatenated without any separator, |
| * and they appear in the cache key in their input order. |
| * |
| * @param input a collection of Cacheables to be included in the cache key |
| * @return this instance |
| */ |
| public CacheKeyBuilder appendCacheables(Collection<? extends Cacheable> input) |
| { |
| appendItem(CACHEABLE_LIST_KEY, cacheableCollectionToByteArray(input, true)); |
| return this; |
| } |
| |
| /** |
| * Add a collection of Cacheables to the cache key. |
| * Cacheables in the collection are sorted by their byte representation and |
| * concatenated without any separator. |
| * |
| * @param input a collection of Cacheables to be included in the cache key |
| * @return this instance |
| */ |
| public CacheKeyBuilder appendCacheablesIgnoringOrder(Collection<? extends Cacheable> input) |
| { |
| appendItem(CACHEABLE_LIST_KEY, cacheableCollectionToByteArray(input, false)); |
| return this; |
| } |
| |
| private void appendItem(byte typeKey, byte[] input) |
| { |
| final Item item = new Item(typeKey, input); |
| items.add(item); |
| size += item.byteSize(); |
| } |
| |
| public byte[] build() |
| { |
| final ByteBuffer buffer = ByteBuffer.allocate(size); |
| buffer.put(id); |
| |
| for (Item item : items) { |
| buffer.put(item.typeKey).put(item.item); |
| } |
| |
| return buffer.array(); |
| } |
| } |