/* * Copyright 2014-2016 CyberVision, Inc. * * Licensed 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.kaaproject.kaa.server.common.core.algorithms.delta; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.DELTA; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.KAA_NAMESPACE; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.RESET; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UNCHANGED; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_FIELD; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_TYPE; import org.apache.avro.Schema; import org.apache.avro.Schema.Field; import org.apache.avro.Schema.Type; import org.apache.avro.generic.GenericArray; import org.apache.avro.generic.GenericContainer; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericEnumSymbol; import org.apache.avro.generic.GenericFixed; import org.apache.avro.generic.GenericRecord; import org.kaaproject.kaa.common.avro.AvroDataCanonizationUtils; import org.kaaproject.kaa.common.avro.GenericAvroConverter; import org.kaaproject.kaa.server.common.core.configuration.BaseData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; /** * Default implementation of {@link DeltaCalculationAlgorithm}. * * @author Yaroslav Zeygerman */ public class DefaultDeltaCalculationAlgorithm implements DeltaCalculationAlgorithm { /** * The Constant LOG. */ private static final Logger LOG = LoggerFactory //NOSONAR .getLogger(DefaultDeltaCalculationAlgorithm.class); /** * The delta schema. */ private final Schema deltaSchema; private final Schema baseSchema; private Set<RecordTuple> processedRecords; /** * The last uuid delta. */ private RecordTuple lastUuidDelta; /** * The last uuid records. */ private RecordTuple lastUuidRecords; /** * The result delta. */ private AvroBinaryDelta resultDelta; /** * Instantiates a new default delta calculator. * * @param deltaSchema the schema * @param baseSchema the schema */ public DefaultDeltaCalculationAlgorithm(Schema deltaSchema, Schema baseSchema) { this.deltaSchema = deltaSchema; this.baseSchema = baseSchema; } /** * Gets the full name. * * @param record the record * @return the full name */ private static String getFullName(GenericContainer record) { return record.getSchema().getFullName(); } /** * Gets the array schema. * * @param delta the delta * @param field the field * @return the array schema */ private static Schema getArraySchema(GenericRecord delta, String field) { List<Schema> fieldTypes = delta.getSchema().getField(field).schema().getTypes(); for (Schema type : fieldTypes) { if (type.getType() == Schema.Type.ARRAY) { return type; } } return null; } /** * Gets the schema by full name. * * @param types the types * @param fullName the full name * @return the schema by full name */ private static Schema getSchemaByFullName(List<Schema> types, String fullName) { for (Schema type : types) { if (type.getFullName().equals(fullName)) { return type; } } return null; } /** * Gets the schema by full name. * * @param delta the delta * @param field the field * @param fullName the full name * @return the schema by full name */ private static Schema getSchemaByFullName(GenericRecord delta, String field, String fullName) { Schema fieldSchema = delta.getSchema().getField(field).schema(); if (fieldSchema.getType() == Schema.Type.UNION) { List<Schema> fieldTypes = fieldSchema.getTypes(); return getSchemaByFullName(fieldTypes, fullName); } else { return fieldSchema.getFullName().equals(fullName) ? fieldSchema : null; } } /** * Gets the schema by full name. * * @param arraySchema the array schema * @param fullName the full name * @return the schema by full name */ private static Schema getSchemaByFullName(Schema arraySchema, String fullName) { if (arraySchema.getElementType().getType() == Schema.Type.UNION) { List<Schema> itemTypes = arraySchema.getElementType().getTypes(); return getSchemaByFullName(itemTypes, fullName); } else { return arraySchema.getElementType().getFullName().equals( fullName) ? arraySchema.getElementType() : null; } } /** * Put notchaged. * * @param delta the delta * @param field the field * @throws DeltaCalculatorException the delta calculator exception */ private static void putUnchanged(GenericRecord delta, String field) throws DeltaCalculatorException { Schema unchangedSchema = getSchemaByFullName( delta, field, KAA_NAMESPACE + "." + UNCHANGED + "T"); if (unchangedSchema != null) { GenericEnumSymbol unchanged = new GenericData.EnumSymbol(unchangedSchema, UNCHANGED); delta.put(field, unchanged); } else { throw new DeltaCalculatorException(new StringBuilder() .append("Failed to find schema for \"unchanged\" type ").append(" in ") .append(delta.getSchema().getFullName()).append(" field ").append(field).toString()); } } /** * Put reset. * * @param delta the delta * @param field the field * @throws DeltaCalculatorException the delta calculator exception */ private static void putReset(GenericRecord delta, String field) throws DeltaCalculatorException { Schema resetSchema = getSchemaByFullName(delta, field, KAA_NAMESPACE + "." + RESET + "T"); if (resetSchema != null) { GenericEnumSymbol reset = new GenericData.EnumSymbol(resetSchema, RESET); delta.put(field, reset); } else { throw new DeltaCalculatorException(new StringBuilder().append( "Failed to find schema for \"reset\" type ").append(" in ").append( delta.getSchema().getFullName()).append(" field ").append(field).toString()); } } /** * Creates the sub delta. * * @param delta the delta * @param field the field * @param record the record * @return the generic record */ private static GenericRecord createSubDelta( GenericRecord delta, String field, GenericRecord record) { Schema recordType = getSchemaByFullName(delta, field, getFullName(record)); return recordType == null ? null : new GenericData.Record(recordType); } /** * Fill delta array fields. * * @param delta the delta * @param resetFields the reset fields * @param uuidFields the uuid fields * @param fieldQueue the field queue * @throws DeltaCalculatorException the delta calculator exception */ private static void fillDeltaArrayFields(GenericRecord delta, Set<String> resetFields, Map<String, List<byte[]>> uuidFields, Queue<FieldAttribute> fieldQueue) throws DeltaCalculatorException { List<Schema.Field> fields = delta.getSchema().getFields(); if (fieldQueue.isEmpty()) { for (Schema.Field field : fields) { if (resetFields.contains(field.name())) { putReset(delta, field.name()); } else if (uuidFields.containsKey(field.name())) { Schema arraySchema = getArraySchema(delta, field.name()); Schema uuidSchema = getSchemaByFullName(arraySchema, KAA_NAMESPACE + "." + UUID_TYPE); List<byte[]> uuids = uuidFields.get(field.name()); GenericArray arrayField = new GenericData.Array(uuids.size(), arraySchema); for (byte[] uuid : uuids) { GenericFixed uuidFixed = new GenericData.Fixed(uuidSchema, uuid); arrayField.add(uuidFixed); } delta.put(field.name(), arrayField); } else if (!field.name().equals(UUID_FIELD)) { putUnchanged(delta, field.name()); } } } else { FieldAttribute nextField = fieldQueue.poll(); for (Schema.Field field : fields) { if (field.name().equals(nextField.getFieldName())) { GenericRecord subDelta = new GenericData.Record(nextField.getFieldSchema()); fillDeltaArrayFields(subDelta, resetFields, uuidFields, fieldQueue); delta.put(field.name(), subDelta); } else if (!field.name().equals(UUID_FIELD)) { putUnchanged(delta, field.name()); } } } } /** * Gets the delta schema by full name. * * @param fullName the full name * @return the delta schema by full name */ private Schema getDeltaSchemaByFullName(String fullName) { Schema deltaT = deltaSchema.getElementType(); Schema deltaUnion = deltaT.getField(DELTA).schema(); List<Schema> deltas = deltaUnion.getTypes(); for (Schema delta : deltas) { if (delta.getFullName().equals(fullName)) { return delta; } } return null; } /** * Adds the complex item to array. * * @param container the record * @param array the array * @throws DeltaCalculatorException the delta calculator exception */ private void addComplexItemToArray(GenericContainer container, GenericArray array) throws DeltaCalculatorException { Schema itemSchema = getSchemaByFullName(array.getSchema(), getFullName(container)); if (itemSchema.getType() == Type.RECORD) { GenericRecord subDelta = new GenericData.Record(itemSchema); fillDeltaWithoutMerge(subDelta, (GenericRecord) container); array.add(subDelta); } else { array.add(container); } } /** * Process complex field. * * @param delta the delta * @param field the field * @param newRecordValue the new record value * @param oldRecordValue the old record value * @param fieldQueue the field queue * @throws DeltaCalculatorException the delta calculator exception */ private void processComplexField( GenericRecord delta, String field, GenericContainer newRecordValue, GenericContainer oldRecordValue, Queue<FieldAttribute> fieldQueue) throws DeltaCalculatorException { boolean fieldChanged = false; if (newRecordValue.getSchema().getType() == Type.RECORD) { GenericRecord subDelta = createSubDelta(delta, field, (GenericRecord) newRecordValue); if (subDelta != null) { boolean hasChanges = false; if (oldRecordValue != null && oldRecordValue.getSchema().getFullName().equals( newRecordValue.getSchema().getFullName())) { FieldAttribute fieldPair = new FieldAttribute(getSchemaByFullName(delta, field, getFullName(newRecordValue)), field); Queue<FieldAttribute> newFieldQueue = new LinkedList<FieldAttribute>(fieldQueue); newFieldQueue.offer(fieldPair); hasChanges = fillDelta(subDelta, (GenericRecord) oldRecordValue, (GenericRecord) newRecordValue, newFieldQueue); } else { fillDeltaWithoutMerge(subDelta, (GenericRecord) newRecordValue); hasChanges = true; } if (hasChanges) { delta.put(field, subDelta); fieldChanged = true; } } else { throw new DeltaCalculatorException( new StringBuilder().append("Failed to find subdelta schema \"") .append(getFullName(newRecordValue)).append("\"").toString()); } } else if (oldRecordValue == null || field.equals(UUID_FIELD) || !newRecordValue.equals(oldRecordValue)) { delta.put(field, newRecordValue); fieldChanged = true; } if (!fieldChanged) { putUnchanged(delta, field); } } /** * Fill delta without merge. * * @param delta the delta * @param root the root * @throws DeltaCalculatorException the delta calculator exception */ private void fillDeltaWithoutMerge(GenericRecord delta, GenericRecord root) throws DeltaCalculatorException { Schema rootSchema = root.getSchema(); for (Field field : rootSchema.getFields()) { Object value = root.get(field.name()); if (value instanceof List) { List<Object> values = (List<Object>) value; Schema arraySchema = getArraySchema(delta, field.name()); GenericArray deltaArray = new GenericData.Array(values.size(), arraySchema); for (Object item : values) { if (item instanceof GenericContainer) { GenericContainer record = (GenericContainer) item; addComplexItemToArray(record, deltaArray); } else { deltaArray.add(item); } } delta.put(field.name(), deltaArray); } else if (value instanceof GenericContainer) { processComplexField(delta, field.name(), (GenericContainer) value, null, null); } else { delta.put(field.name(), value); } } } /** * Fill delta. * * @param delta the delta * @param oldRoot the old root * @param newRoot the new root * @param fieldQueue the field queue * @return true, if successful * @throws DeltaCalculatorException the delta calculator exception */ private boolean fillDelta(GenericRecord delta, GenericRecord oldRoot, GenericRecord newRoot, Queue<FieldAttribute> fieldQueue) throws DeltaCalculatorException { boolean hasChanges = false; Set<String> resetFields = new HashSet<String>(); Map<String, List<byte[]>> uuidsToRemove = new HashMap<String, List<byte[]>>(); Schema oldSchema = oldRoot.getSchema(); Schema newSchema = newRoot.getSchema(); if (oldSchema.getField(UUID_FIELD) != null) { lastUuidDelta = new RecordTuple(oldRoot, newRoot); fieldQueue.clear(); } RecordTuple currentUuidRecord = lastUuidDelta; for (Field newField : newSchema.getFields()) { Object oldValue = oldRoot.get(newField.name()); Object newValue = newRoot.get(newField.name()); if (newValue instanceof List) { List<byte[]> uuids = new LinkedList<byte[]>(); List<Object> oldArrayItems = (oldValue instanceof List) ? (List) oldValue : null; List<Object> newArrayItems = new LinkedList<Object>((List<Object>) newValue); if (!newArrayItems.isEmpty()) { if (newArrayItems.get(0) instanceof GenericRecord) { // Item is a complex type if (oldArrayItems != null && !oldArrayItems.isEmpty() && oldArrayItems.get(0) instanceof GenericRecord) { for (Object oldItem : oldArrayItems) { GenericRecord oldItemRecord = (GenericRecord) oldItem; Schema oldItemSchema = oldItemRecord.getSchema(); if (oldItemSchema.getField(UUID_FIELD) != null) { // Addressable array item. Looking for the // record with the same uuid in new items boolean isRecordExists = false; GenericFixed uuid = (GenericFixed) oldItemRecord.get(UUID_FIELD); Iterator it = newArrayItems.iterator(); while (it.hasNext()) { GenericRecord newItemRecord = (GenericRecord) it.next(); if (uuid.equals(newItemRecord.get(UUID_FIELD))) { processDifferences(oldItemRecord, newItemRecord); isRecordExists = true; // This new item has been processed. // Removing it from the list it.remove(); break; } } if (!isRecordExists) { // Adding uuid to list to remove this // item GenericFixed uuidFixed = (GenericFixed) oldItemRecord.get(UUID_FIELD); byte[] uuidRaw = uuidFixed.bytes(); uuids.add(uuidRaw); hasChanges = true; } } else { // Non-addressable complex item. We can't // create the partial update delta for it boolean itemChanged = true; if (oldArrayItems.size() == newArrayItems.size()) { Iterator it = newArrayItems.iterator(); while (it.hasNext()) { GenericRecord newItemRecord = (GenericRecord) it.next(); if (newItemRecord.equals(oldItemRecord)) { // This new item has been // processed. Removing it from // the list it.remove(); itemChanged = false; break; } } } if (itemChanged) { resetFields.add(newField.name()); newArrayItems = new LinkedList<Object>((List<Object>) newValue); hasChanges = true; break; } } } } else if (oldArrayItems != null && !oldArrayItems.isEmpty()) { resetFields.add(newField.name()); } if (!newArrayItems.isEmpty()) { Schema arraySchema = getArraySchema(delta, newField.name()); GenericArray deltaArray = new GenericData.Array(newArrayItems.size(), arraySchema); // Adding all new elements to delta for (Object item : newArrayItems) { GenericContainer newItemRecord = (GenericContainer) item; addComplexItemToArray(newItemRecord, deltaArray); } delta.put(newField.name(), deltaArray); hasChanges = true; } else { putUnchanged(delta, newField.name()); } } else { // Item is a primitive type if (!newArrayItems.equals(oldArrayItems)) { // Field should be reseted if (oldArrayItems != null) { resetFields.add(newField.name()); } // Adding all elements from new array Schema arraySchema = getArraySchema(delta, newField.name()); GenericArray deltaArray = new GenericData.Array(arraySchema, newArrayItems); delta.put(newField.name(), deltaArray); hasChanges = true; } else { putUnchanged(delta, newField.name()); } } } else if (oldArrayItems == null) { delta.put(newField.name(), new GenericData.Array(0, getArraySchema(delta, newField.name()))); hasChanges = true; } else if (!oldArrayItems.isEmpty()) { resetFields.add(newField.name()); hasChanges = true; } else { putUnchanged(delta, newField.name()); } if (!uuids.isEmpty()) { if (oldArrayItems != null && uuids.size() == oldArrayItems.size()) { // If all items should be deleted, we can set "reset" to // this field resetFields.add(newField.name()); } else { uuidsToRemove.put(newField.name(), uuids); } } } else if (newValue instanceof GenericContainer) { GenericContainer newRecordValue = (GenericContainer) newValue; GenericContainer oldRecordValue = (oldValue instanceof GenericContainer) ? (GenericContainer) oldValue : null; processComplexField(delta, newField.name(), newRecordValue, oldRecordValue, fieldQueue); } else if ((newValue == null && oldValue != null) || (newValue != null && !newValue.equals(oldValue))) { delta.put(newField.name(), newValue); hasChanges = true; } else { putUnchanged(delta, newField.name()); } } lastUuidDelta = currentUuidRecord; if (!uuidsToRemove.isEmpty() || !resetFields.isEmpty()) { GenericRecord arrayDelta = new GenericData.Record( getDeltaSchemaByFullName(getFullName(lastUuidDelta.getNewRecord()))); GenericFixed uuid = (GenericFixed) lastUuidDelta.getNewRecord().get(UUID_FIELD); arrayDelta.put(UUID_FIELD, uuid); Queue<FieldAttribute> newFieldQueue = new LinkedList<FieldAttribute>(fieldQueue); fillDeltaArrayFields(arrayDelta, resetFields, uuidsToRemove, newFieldQueue); resultDelta.addDelta(arrayDelta); } return hasChanges; } /** * Process differences. * * @param oldRoot the old root * @param newRoot the new root * @throws DeltaCalculatorException the delta calculator exception */ private void processDifferences(GenericRecord oldRoot, GenericRecord newRoot) throws DeltaCalculatorException { Schema oldSchema = oldRoot.getSchema(); boolean hasDifferences = false; if (oldSchema.getField(UUID_FIELD) != null) { if (oldRoot.get(UUID_FIELD).equals(newRoot.get(UUID_FIELD))) { lastUuidRecords = new RecordTuple(oldRoot, newRoot); } else { hasDifferences = true; } } LinkedList<RecordTuple> nextRecords = new LinkedList<RecordTuple>(); //NOSONAR if (!hasDifferences) { for (Field oldField : oldSchema.getFields()) { Object newValue = newRoot.get(oldField.name()); Object oldValue = oldRoot.get(oldField.name()); if (newValue instanceof GenericRecord && oldValue instanceof GenericRecord) { GenericRecord oldRecord = (GenericRecord) oldValue; GenericRecord newRecord = (GenericRecord) newValue; if (oldRecord.getSchema().getFullName().equals(newRecord.getSchema().getFullName())) { nextRecords.offer(new RecordTuple(oldRecord, newRecord)); } else { hasDifferences = true; break; } } else if (newValue instanceof List && oldValue instanceof List) { List<Object> oldArray = (List<Object>) oldValue; List<Object> newArray = (List<Object>) newValue; hasDifferences = !(oldArray.size() == newArray.size()); if (!hasDifferences) { if (!newArray.isEmpty() && newArray.get(0) instanceof GenericRecord && oldArray.get(0) instanceof GenericRecord) { GenericRecord uuidCheckRecord = (GenericRecord) newArray.get(0); Schema uuidCheckSchema = uuidCheckRecord.getSchema(); if (uuidCheckSchema.getField(UUID_FIELD) != null) { for (Object oldItem : oldArray) { GenericRecord oldItemRecord = (GenericRecord) oldItem; boolean isRecordExists = false; GenericFixed uuid = (GenericFixed) oldItemRecord.get(UUID_FIELD); if (uuid != null) { for (Object it : newArray) { GenericRecord newItemRecord = (GenericRecord) it; GenericFixed newUuid = (GenericFixed) newItemRecord.get(UUID_FIELD); if (uuid.equals(newUuid)) { nextRecords.offer(new RecordTuple(oldItemRecord, newItemRecord)); isRecordExists = true; break; } } if (!isRecordExists) { hasDifferences = true; break; } } else { hasDifferences = true; break; } } } else { hasDifferences = !newArray.equals(oldArray); } if (hasDifferences) { break; } } else if (!newArray.equals(oldArray)) { hasDifferences = true; break; } } } else if ((newValue == null && oldValue != null) || (newValue != null && !newValue.equals(oldValue))) { hasDifferences = true; break; } } } String lastUuidRecordName = getFullName(lastUuidRecords.getOldRecord()); if (hasDifferences && !processedRecords.contains(lastUuidRecords)) { Schema deltaSubSchema = getDeltaSchemaByFullName(lastUuidRecordName); if (deltaSubSchema == null) { throw new DeltaCalculatorException( new StringBuilder().append("Failed to find schema for \"") .append(lastUuidRecordName).append("\"").toString()); } GenericRecord delta = new GenericData.Record(deltaSubSchema); fillDelta(delta, lastUuidRecords.getOldRecord(), lastUuidRecords.getNewRecord(), new LinkedList<FieldAttribute>()); resultDelta.addDelta(delta); processedRecords.add(lastUuidRecords); } else { RecordTuple currentUuidRecords = lastUuidRecords; for (RecordTuple tuple : nextRecords) { processDifferences(tuple.getOldRecord(), tuple.getNewRecord()); if (!lastUuidRecords.equals(currentUuidRecords)) { lastUuidRecords = currentUuidRecords; } } } } /** * Calculates a delta. * * @param endpointConfiguration old configuration data (binary) * @param newConfigurationBody the new configuration body (binary) * @return delta * @throws IOException the io exception * @throws DeltaCalculatorException the delta calculation exception */ @Override public RawBinaryDelta calculate(BaseData endpointConfiguration, BaseData newConfigurationBody) throws IOException, DeltaCalculatorException { GenericRecord oldRoot = getRootNode(endpointConfiguration, baseSchema); GenericRecord newRoot = getRootNode(newConfigurationBody, baseSchema); return calculate(oldRoot, newRoot); } /** * Calculates a delta. * * @param newConfigurationBody the new configuration body (binary) * @return delta * @throws IOException the io exception * @throws DeltaCalculatorException the delta calculation exception */ @Override public RawBinaryDelta calculate(BaseData newConfigurationBody) throws IOException, DeltaCalculatorException { GenericRecord newRoot = getRootNode(newConfigurationBody, baseSchema); return calculate(newRoot); } /** * Calculates a delta. * * @param oldConfig old config * @param newConfig new config * @return delta * @throws DeltaCalculatorException the delta calculation exception */ public RawBinaryDelta calculate(GenericRecord oldConfig, GenericRecord newConfig) throws DeltaCalculatorException { resultDelta = new AvroBinaryDelta(deltaSchema); processedRecords = new HashSet<>(); processDifferences(oldConfig, newConfig); return resultDelta; } /** * Calculates a delta. * * @param newConfig new config * @return delta * @throws DeltaCalculatorException the delta calculation exception */ public RawBinaryDelta calculate(GenericRecord newConfig) throws DeltaCalculatorException { resultDelta = new AvroBinaryDelta(deltaSchema); Schema deltaSubSchema = getDeltaSchemaByFullName(getFullName(newConfig)); if (deltaSubSchema == null) { throw new DeltaCalculatorException(new StringBuilder().append("Failed to find schema for \"") .append(getFullName(newConfig)).append("\"").toString()); } GenericRecord delta = new GenericData.Record(deltaSubSchema); fillDeltaWithoutMerge(delta, newConfig); AvroDataCanonizationUtils.canonizeRecord(delta); resultDelta.addDelta(delta); return resultDelta; } /** * Gets the root node. * * @param data the base data object * @return the root node */ private GenericRecord getRootNode(BaseData data, Schema schema) throws IOException { GenericAvroConverter<GenericRecord> converter = new GenericAvroConverter<GenericRecord>(schema); return converter.decodeJson(data.getRawData()); } /** * The Class FieldAttribute. */ private class FieldAttribute { /** * The field schema. */ private final Schema fieldSchema; /** * The field name. */ private final String fieldName; /** * Instantiates a new field attribute. * * @param fieldSchema the field schema * @param fieldName the field name */ public FieldAttribute(Schema fieldSchema, String fieldName) { this.fieldSchema = fieldSchema; this.fieldName = fieldName; } /** * Gets the field schema. * * @return the field schema */ public Schema getFieldSchema() { return fieldSchema; } /** * Gets the field name. * * @return the field name */ public String getFieldName() { return fieldName; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + ((fieldName == null) ? 0 : fieldName.hashCode()); result = prime * result + ((fieldSchema == null) ? 0 : fieldSchema.hashCode()); return result; } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } FieldAttribute other = (FieldAttribute) obj; if (!getOuterType().equals(other.getOuterType())) { return false; } if (fieldName == null) { if (other.fieldName != null) { return false; } } else if (!fieldName.equals(other.fieldName)) { return false; } if (fieldSchema == null) { if (other.fieldSchema != null) { return false; } } else if (!fieldSchema.equals(other.fieldSchema)) { return false; } return true; } /** * Gets the outer type. * * @return the outer type */ private DefaultDeltaCalculationAlgorithm getOuterType() { return DefaultDeltaCalculationAlgorithm.this; } } /** * The Class RecordTuple. */ private class RecordTuple { /** * The old record. */ private final GenericRecord oldRecord; /** * The new record. */ private final GenericRecord newRecord; /** * Instantiates a new record tuple. * * @param oldRecord the old record * @param newRecord the new record */ public RecordTuple(GenericRecord oldRecord, GenericRecord newRecord) { this.oldRecord = oldRecord; this.newRecord = newRecord; } /** * Gets the old record. * * @return the old record */ public GenericRecord getOldRecord() { return oldRecord; } /** * Gets the new record. * * @return the new record */ public GenericRecord getNewRecord() { return newRecord; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + ((newRecord == null) ? 0 : newRecord.hashCode()); result = prime * result + ((oldRecord == null) ? 0 : oldRecord.hashCode()); return result; } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } RecordTuple other = (RecordTuple) obj; if (!getOuterType().equals(other.getOuterType())) { return false; } if (newRecord == null) { if (other.newRecord != null) { return false; } } else if (!newRecord.equals(other.newRecord)) { return false; } if (oldRecord == null) { if (other.oldRecord != null) { return false; } } else if (!oldRecord.equals(other.oldRecord)) { return false; } return true; } /** * Gets the outer type. * * @return the outer type */ private DefaultDeltaCalculationAlgorithm getOuterType() { return DefaultDeltaCalculationAlgorithm.this; } } }