/* * 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.ignite.internal.binary; import org.apache.ignite.internal.util.typedef.internal.S; import java.io.DataInput; import java.io.DataOutput; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; /** * Schema describing binary object content. We rely on the following assumptions: * - When amount of fields in the object is low, it is better to inline these values into int fields thus allowing * for quick comparisons performed within already fetched L1 cache line. * - When there are more fields, we store them inside a hash map. */ public class BinarySchema implements Externalizable { /** */ private static final long serialVersionUID = 0L; /** Order returned if field is not found. */ public static final int ORDER_NOT_FOUND = -1; /** Minimum sensible size. */ private static final int MAP_MIN_SIZE = 32; /** Empty cell. */ private static final int MAP_EMPTY = 0; /** Schema ID. */ private int schemaId; /** IDs depending on order. */ private int[] ids; /** Interned names of associated fields. */ private String[] names; /** ID-to-order data. */ private int[] idToOrderData; /** ID-to-order mask. */ private int idToOrderMask; /** ID 1. */ private int id0; /** ID 2. */ private int id1; /** ID 3. */ private int id2; /** ID 4. */ private int id3; /** * {@link Externalizable} support. */ public BinarySchema() { // No-op. } /** * Constructor. * * @param schemaId Schema ID. * @param fieldIds Field IDs. */ public BinarySchema(int schemaId, List<Integer> fieldIds) { assert fieldIds != null; this.schemaId = schemaId; initialize(fieldIds); } /** * @return Schema ID. */ public int schemaId() { return schemaId; } /** * Try speculatively confirming order for the given field name. * * @param expOrder Expected order. * @param expName Expected name. * @return Field ID. */ @SuppressWarnings("StringEquality") public Confirmation confirmOrder(int expOrder, String expName) { assert expName != null; if (expOrder < names.length) { String name = names[expOrder]; // Note that we use only reference equality assuming that field names are interned literals. if (name == expName) return Confirmation.CONFIRMED; if (name == null) return Confirmation.CLARIFY; } return Confirmation.REJECTED; } /** * Add field name. * * @param order Order. * @param name Name. */ public void clarifyFieldName(int order, String name) { assert name != null; assert order < names.length; names[order] = name.intern(); } /** * Get field ID by order in footer. * * @param order Order. * @return Field ID. */ public int fieldId(int order) { return order < ids.length ? ids[order] : 0; } /** * Get field order in footer by field ID. * * @param id Field ID. * @return Offset or {@code 0} if there is no such field. */ public int order(int id) { if (idToOrderData == null) { if (id == id0) return 0; if (id == id1) return 1; if (id == id2) return 2; if (id == id3) return 3; return ORDER_NOT_FOUND; } else { int idx = (id & idToOrderMask) << 1; int curId = idToOrderData[idx]; if (id == curId) // Hit! return idToOrderData[idx + 1]; else if (curId == MAP_EMPTY) // No such ID! return ORDER_NOT_FOUND; else { // Unlikely collision scenario. for (int i = 2; i < idToOrderData.length; i += 2) { int newIdx = (idx + i) % idToOrderData.length; assert newIdx < idToOrderData.length - 1; curId = idToOrderData[newIdx]; if (id == curId) return idToOrderData[newIdx + 1]; else if (curId == MAP_EMPTY) return ORDER_NOT_FOUND; } return ORDER_NOT_FOUND; } } } /** {@inheritDoc} */ @Override public int hashCode() { return schemaId; } /** {@inheritDoc} */ @Override public boolean equals(Object o) { return o != null && o instanceof BinarySchema && schemaId == ((BinarySchema)o).schemaId; } /** {@inheritDoc} */ @Override public String toString() { return S.toString(BinarySchema.class, this, "ids", Arrays.toString(ids), "names", Arrays.toString(names), "idToOrderData", Arrays.toString(idToOrderData)); } /** {@inheritDoc} */ @Override public void writeExternal(ObjectOutput out) throws IOException { writeTo(out); } /** * The object implements the writeTo method to save its contents * by calling the methods of DataOutput for its primitive values and strings or * calling the writeTo method for other objects. * * @param out the stream to write the object to. * @throws IOException Includes any I/O exceptions that may occur. */ public void writeTo(DataOutput out) throws IOException { out.writeInt(schemaId); out.writeInt(ids.length); for (Integer id : ids) out.writeInt(id); } /** {@inheritDoc} */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { readFrom(in); } /** * The object implements the readFrom method to restore its * contents by calling the methods of DataInput for primitive * types and strings or calling readFrom for other objects. The * readFrom method must read the values in the same sequence * and with the same types as were written by writeTo. * * @param in the stream to read data from in order to restore the object * @throws IOException if I/O errors occur */ public void readFrom(DataInput in) throws IOException { schemaId = in.readInt(); int idsCnt = in.readInt(); List<Integer> fieldIds = new ArrayList<>(idsCnt); for (int i = 0; i < idsCnt; i++) fieldIds.add(in.readInt()); initialize(fieldIds); } /** * Gets field ids array. * * @return Field ids. */ public int[] fieldIds() { return ids; } /** * Parse values. * * @param vals Values. * @param size Proposed result size. * @return Parse result. */ private static ParseResult parse(int[] vals, int size) { int mask = maskForPowerOfTwo(size); int totalSize = size * 2; int[] data = new int[totalSize]; int collisions = 0; for (int order = 0; order < vals.length; order++) { int id = vals[order]; assert id != 0; int idIdx = (id & mask) << 1; if (data[idIdx] == 0) { // Found empty slot. data[idIdx] = id; data[idIdx + 1] = order; } else { // Collision! collisions++; boolean placeFound = false; for (int i = 2; i < totalSize; i += 2) { int newIdIdx = (idIdx + i) % totalSize; if (data[newIdIdx] == 0) { data[newIdIdx] = id; data[newIdIdx + 1] = order; placeFound = true; break; } } assert placeFound : "Should always have a place for entry!"; } } return new ParseResult(data, collisions); } /** * Get next power of two which greater or equal to the given number. * This implementation is not meant to be very efficient, so it is expected to be used relatively rare. * * @param val Number * @return Nearest pow2. */ private static int nextPowerOfTwo(int val) { int res = 1; while (res < val) res = res << 1; if (res < 0) throw new IllegalArgumentException("Value is too big to find positive pow2: " + val); return res; } /** * Calculate mask for the given value which is a power of two. * * @param val Value. * @return Mask. */ private static int maskForPowerOfTwo(int val) { int mask = 0; int comparand = 1; while (comparand < val) { mask |= comparand; comparand <<= 1; } return mask; } /** * Initialization routine. * * @param fieldIds Field IDs. */ private void initialize(List<Integer> fieldIds) { ids = new int[fieldIds.size()]; for (int i = 0; i < fieldIds.size(); i++) ids[i] = fieldIds.get(i); names = new String[fieldIds.size()]; if (fieldIds.size() <= 4) { Iterator<Integer> iter = fieldIds.iterator(); id0 = iter.hasNext() ? iter.next() : 0; id1 = iter.hasNext() ? iter.next() : 0; id2 = iter.hasNext() ? iter.next() : 0; id3 = iter.hasNext() ? iter.next() : 0; } else { id0 = id1 = id2 = id3 = 0; initializeMap(ids); } } /** * Initialize the map. * * @param vals Values. */ private void initializeMap(int[] vals) { int size = Math.max(nextPowerOfTwo(vals.length) << 2, MAP_MIN_SIZE); assert size > 0; ParseResult finalRes; ParseResult res1 = parse(vals, size); if (res1.collisions == 0) finalRes = res1; else { ParseResult res2 = parse(vals, size * 2); // Failed to decrease aom if (res2.collisions == 0) finalRes = res2; else finalRes = parse(vals, size * 4); } idToOrderData = finalRes.data; idToOrderMask = maskForPowerOfTwo(idToOrderData.length / 2); } /** * Schema builder. */ public static class Builder { /** Schema ID. */ private int schemaId = BinaryUtils.schemaInitialId(); /** Fields. */ private final ArrayList<Integer> fields = new ArrayList<>(); /** * Create new schema builder. * * @return Schema builder. */ public static Builder newBuilder() { return new Builder(); } /** * Private constructor. */ private Builder() { // No-op. } /** * Add field. * * @param fieldId Field ID. */ public void addField(int fieldId) { fields.add(fieldId); schemaId = BinaryUtils.updateSchemaId(schemaId, fieldId); } /** * Build schema. * * @return Schema. */ public BinarySchema build() { return new BinarySchema(schemaId, fields); } } /** * Order confirmation result. */ public enum Confirmation { /** Confirmed. */ CONFIRMED, /** Denied. */ REJECTED, /** Field name clarification is needed. */ CLARIFY } /** * Result of map parsing. */ private static class ParseResult { /** Data. */ private int[] data; /** Collisions. */ private int collisions; /** * Constructor. * * @param data Data. * @param collisions Collisions. */ private ParseResult(int[] data, int collisions) { this.data = data; this.collisions = collisions; } } }