/* * 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.nifi.serialization.record; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; import org.apache.nifi.serialization.record.type.ArrayDataType; import org.apache.nifi.serialization.record.type.MapDataType; import org.apache.nifi.serialization.record.util.DataTypeUtils; import org.apache.nifi.serialization.record.util.IllegalTypeConversionException; public class MapRecord implements Record { private RecordSchema schema; private final Map<String, Object> values; private Optional<SerializedForm> serializedForm; public MapRecord(final RecordSchema schema, final Map<String, Object> values) { this.schema = Objects.requireNonNull(schema); this.values = Objects.requireNonNull(values); this.serializedForm = Optional.empty(); } public MapRecord(final RecordSchema schema, final Map<String, Object> values, final SerializedForm serializedForm) { this.schema = Objects.requireNonNull(schema); this.values = Objects.requireNonNull(values); this.serializedForm = Optional.ofNullable(serializedForm); } @Override public RecordSchema getSchema() { return schema; } @Override public Object[] getValues() { final Object[] values = new Object[schema.getFieldCount()]; int i = 0; for (final RecordField recordField : schema.getFields()) { values[i++] = getValue(recordField); } return values; } @Override public Object getValue(final String fieldName) { final Optional<RecordField> fieldOption = schema.getField(fieldName); if (fieldOption.isPresent()) { return getValue(fieldOption.get()); } return null; } @Override public Object getValue(final RecordField field) { Object explicitValue = getExplicitValue(field); if (explicitValue != null) { return explicitValue; } final Optional<RecordField> resolvedField = resolveField(field); final boolean resolvedFieldDifferent = resolvedField.isPresent() && !resolvedField.get().equals(field); if (resolvedFieldDifferent) { explicitValue = getExplicitValue(resolvedField.get()); if (explicitValue != null) { return explicitValue; } } Object defaultValue = field.getDefaultValue(); if (defaultValue != null) { return defaultValue; } if (resolvedFieldDifferent) { return resolvedField.get().getDefaultValue(); } return null; } private Optional<RecordField> resolveField(final RecordField field) { Optional<RecordField> resolved = schema.getField(field.getFieldName()); if (resolved.isPresent()) { return resolved; } for (final String alias : field.getAliases()) { resolved = schema.getField(alias); if (resolved.isPresent()) { return resolved; } } return Optional.empty(); } private Object getExplicitValue(final RecordField field) { final String canonicalFieldName = field.getFieldName(); // We use containsKey here instead of just calling get() and checking for a null value // because if the true field name is set to null, we want to return null, rather than // what the alias points to. Likewise for a specific alias, since aliases are defined // in a List with a specific ordering. Object value = values.get(canonicalFieldName); if (value != null) { return value; } for (final String alias : field.getAliases()) { value = values.get(alias); if (value != null) { return value; } } return null; } @Override public String getAsString(final String fieldName) { final Optional<DataType> dataTypeOption = schema.getDataType(fieldName); if (!dataTypeOption.isPresent()) { return null; } return convertToString(getValue(fieldName), dataTypeOption.get().getFormat()); } @Override public String getAsString(final String fieldName, final String format) { return convertToString(getValue(fieldName), format); } @Override public String getAsString(final RecordField field, final String format) { return convertToString(getValue(field), format); } private String convertToString(final Object value, final String format) { if (value == null) { return null; } return DataTypeUtils.toString(value, format); } @Override public Long getAsLong(final String fieldName) { return DataTypeUtils.toLong(getValue(fieldName), fieldName); } @Override public Integer getAsInt(final String fieldName) { return DataTypeUtils.toInteger(getValue(fieldName), fieldName); } @Override public Double getAsDouble(final String fieldName) { return DataTypeUtils.toDouble(getValue(fieldName), fieldName); } @Override public Float getAsFloat(final String fieldName) { return DataTypeUtils.toFloat(getValue(fieldName), fieldName); } @Override public Record getAsRecord(String fieldName, final RecordSchema schema) { return DataTypeUtils.toRecord(getValue(fieldName), schema, fieldName); } @Override public Boolean getAsBoolean(final String fieldName) { return DataTypeUtils.toBoolean(getValue(fieldName), fieldName); } @Override public Date getAsDate(final String fieldName, final String format) { return DataTypeUtils.toDate(getValue(fieldName), () -> DataTypeUtils.getDateFormat(format), fieldName); } @Override public Object[] getAsArray(final String fieldName) { return DataTypeUtils.toArray(getValue(fieldName), fieldName); } @Override public int hashCode() { return 31 + 41 * values.hashCode() + 7 * schema.hashCode(); } @Override public boolean equals(final Object obj) { if (obj == this) { return true; } if (obj == null) { return false; } if (!(obj instanceof MapRecord)) { return false; } final MapRecord other = (MapRecord) obj; return schema.equals(other.schema) && values.equals(other.values); } @Override public String toString() { return "MapRecord[" + values + "]"; } @Override public Optional<SerializedForm> getSerializedForm() { return serializedForm; } @Override public void setValue(final String fieldName, final Object value) { final Optional<RecordField> field = getSchema().getField(fieldName); if (!field.isPresent()) { return; } final RecordField recordField = field.get(); final Object coerced = DataTypeUtils.convertType(value, recordField.getDataType(), fieldName); final Object previousValue = values.put(recordField.getFieldName(), coerced); if (!Objects.equals(coerced, previousValue)) { serializedForm = Optional.empty(); } } @Override public void setArrayValue(final String fieldName, final int arrayIndex, final Object value) { final Optional<RecordField> field = getSchema().getField(fieldName); if (!field.isPresent()) { return; } final RecordField recordField = field.get(); final DataType dataType = recordField.getDataType(); if (dataType.getFieldType() != RecordFieldType.ARRAY) { throw new IllegalTypeConversionException("Cannot set the value of an array index on Record because the field '" + fieldName + "' is of type '" + dataType + "' and cannot be coerced into an ARRAY type"); } final Object arrayObject = values.get(recordField.getFieldName()); if (arrayObject == null) { return; } if (!(arrayObject instanceof Object[])) { return; } final Object[] array = (Object[]) arrayObject; if (arrayIndex >= array.length) { return; } final ArrayDataType arrayDataType = (ArrayDataType) dataType; final DataType elementType = arrayDataType.getElementType(); final Object coerced = DataTypeUtils.convertType(value, elementType, fieldName); final boolean update = !Objects.equals(coerced, array[arrayIndex]); if (update) { array[arrayIndex] = coerced; serializedForm = Optional.empty(); } } @Override @SuppressWarnings("unchecked") public void setMapValue(final String fieldName, final String mapKey, final Object value) { final Optional<RecordField> field = getSchema().getField(fieldName); if (!field.isPresent()) { return; } final RecordField recordField = field.get(); final DataType dataType = recordField.getDataType(); if (dataType.getFieldType() != RecordFieldType.MAP) { throw new IllegalTypeConversionException("Cannot set the value of map entry on Record because the field '" + fieldName + "' is of type '" + dataType + "' and cannot be coerced into an MAP type"); } Object mapObject = values.get(recordField.getFieldName()); if (mapObject == null) { mapObject = new HashMap<String, Object>(); } if (!(mapObject instanceof Map)) { return; } final Map<String, Object> map = (Map<String, Object>) mapObject; final MapDataType mapDataType = (MapDataType) dataType; final DataType valueDataType = mapDataType.getValueType(); final Object coerced = DataTypeUtils.convertType(value, valueDataType, fieldName); final Object replaced = map.put(mapKey, coerced); if (replaced == null || !replaced.equals(coerced)) { serializedForm = Optional.empty(); } } @Override public void incorporateSchema(RecordSchema other) { this.schema = DataTypeUtils.merge(this.schema, other); } }