/* * Copyright © 2014 Cask Data, 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 co.cask.cdap.api.data.format; import co.cask.cdap.api.annotation.Beta; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.api.data.schema.Schema; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * Instance of a record structured by a {@link Schema}. Fields are accessible by name. */ @Beta public class StructuredRecord implements Serializable { private final Schema schema; private final Map<String, Object> fields; private static final long serialVersionUID = -4648752378975451591L; private StructuredRecord(Schema schema, Map<String, Object> fields) { this.schema = schema; this.fields = fields; } /** * Get the schema of the record. * * @return schema of the record. */ public Schema getSchema() { return schema; } /** * Get the value of a field in the record. * * @param fieldName field to get. * @param <T> type of object of the field value. * @return value of the field. */ @SuppressWarnings("unchecked") public <T> T get(String fieldName) { return (T) fields.get(fieldName); } /** * Get a builder for creating a record with the given schema. * * @param schema schema for the record to build. * @return builder for creating a record with the given schema. * @throws UnexpectedFormatException if the given schema is not a record with at least one field. */ public static Builder builder(Schema schema) throws UnexpectedFormatException { if (schema == null || schema.getType() != Schema.Type.RECORD || schema.getFields().size() < 1) { throw new UnexpectedFormatException("Schema must be a record with at least one field."); } return new Builder(schema); } /** * Builder for creating a {@link StructuredRecord}. * TODO: enforce schema correctness? */ public static class Builder { private final Schema schema; private Map<String, Object> fields; private Builder(Schema schema) { this.schema = schema; this.fields = new HashMap<>(); } /** * Set the field to the given value. * * @param fieldName Name of the field to set. * @param value Value for the field. * @return This builder. * @throws UnexpectedFormatException if the field is not in the schema, or the field is not nullable but a null * value is given. */ public Builder set(String fieldName, Object value) { validateAndGetField(fieldName, value); fields.put(fieldName, value); return this; } /** * Convert the given string into the type of the given field, and set the value for that field. A String can be * converted to a boolean, int, long, float, double, bytes, string, or null. * * @param fieldName Name of the field to set. * @param strVal String value for the field. * @return This builder. * @throws UnexpectedFormatException if the field is not in the schema, or the field is not nullable but a null * value is given, or the string cannot be converted to the type for the field. */ public Builder convertAndSet(String fieldName, String strVal) throws UnexpectedFormatException { Schema.Field field = validateAndGetField(fieldName, strVal); fields.put(fieldName, convertString(field.getSchema(), strVal)); return this; } /** * Build a {@link StructuredRecord} with the fields set by this builder. * * @return A {@link StructuredRecord} with the fields set by this builder. * @throws UnexpectedFormatException if there is at least one non-nullable field without a value. */ public StructuredRecord build() throws UnexpectedFormatException { // check that all non-nullable fields have a value. for (Schema.Field field : schema.getFields()) { String fieldName = field.getName(); if (!fields.containsKey(fieldName)) { // if the field is not nullable and there is no value set for the field, this is invalid. if (!field.getSchema().isNullable()) { throw new UnexpectedFormatException("Field " + fieldName + " must contain a value."); } else { // otherwise, set the value for the field to null fields.put(fieldName, null); } } } return new StructuredRecord(schema, fields); } private Object convertString(Schema schema, String strVal) throws UnexpectedFormatException { Schema.Type simpleType; if (schema.getType().isSimpleType()) { simpleType = schema.getType(); if (strVal == null && simpleType != Schema.Type.NULL) { throw new UnexpectedFormatException("Cannot set non-nullable field to a null value."); } } else if (schema.isNullable()) { if (strVal == null) { return null; } simpleType = schema.getNonNullable().getType(); } else { throw new UnexpectedFormatException("Cannot convert a string to schema " + schema); } switch (simpleType) { case BOOLEAN: return Boolean.parseBoolean(strVal); case INT: return Integer.parseInt(strVal); case LONG: return Long.parseLong(strVal); case FLOAT: return Float.parseFloat(strVal); case DOUBLE: return Double.parseDouble(strVal); case BYTES: return Bytes.toBytesBinary(strVal); case STRING: return strVal; case NULL: return null; default: // shouldn't ever get here throw new UnexpectedFormatException("Cannot convert a string to schema " + schema); } } private Schema.Field validateAndGetField(String fieldName, Object val) { Schema.Field field = schema.getField(fieldName); if (field == null) { throw new UnexpectedFormatException("field " + fieldName + " is not in the schema."); } if (!field.getSchema().isNullable() && val == null) { throw new UnexpectedFormatException("field " + fieldName + " cannot be set to a null value."); } return field; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } StructuredRecord that = (StructuredRecord) o; return Objects.equals(schema, that.schema) && Objects.equals(fields, that.fields); } @Override public int hashCode() { return Objects.hash(schema, fields); } }