/** * Copyright 2013 Cloudera 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 com.cloudera.cdk.data.hbase.avro; import com.cloudera.cdk.data.DatasetException; import com.cloudera.cdk.data.SchemaValidationException; import com.cloudera.cdk.data.hbase.impl.EntitySchema; import com.google.common.base.Objects; import java.io.IOException; import java.util.Collection; import org.apache.avro.Schema; import org.apache.avro.Schema.Field; import org.apache.avro.io.parsing.ResolvingGrammarGenerator; import org.apache.avro.io.parsing.Symbol; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; /** * An EntitySchema implementation powered by Avro. */ public class AvroEntitySchema extends EntitySchema { private final Schema schema; /** * Constructor for the AvroEntitySchema. * * @param tables * The tables this EntitySchema can be persisted to * @param schema * The Avro Schema that underlies this EntitySchema implementation * @param rawSchema * The Avro Schema as a string that underlies the EntitySchema * implementation * @param fieldMappings * The list of FieldMappings that specify how each field maps to an * HBase row */ public AvroEntitySchema(Collection<String> tables, Schema schema, String rawSchema, Collection<FieldMapping> fieldMappings) { super(tables, schema.getName(), rawSchema, fieldMappings); this.schema = schema; } /** * Get the Avro Schema that underlies this EntitySchema implementation. * * @return The Avro Schema */ public Schema getAvroSchema() { return schema; } public Schema getKeyAvroSchema() { return null; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((schema == null) ? 0 : schema.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; AvroEntitySchema other = (AvroEntitySchema) obj; if (schema == null) { if (other.schema != null) return false; } else { if (schema.getFields().size() != other.getAvroSchema().getFields().size()) { return false; } for (Field field : schema.getFields()) { Field entitySchemaField = other.getAvroSchema().getFields() .get(field.pos()); if (!fieldsEqual(field, getFieldMapping(field.name()), entitySchemaField, other.getFieldMapping(entitySchemaField.name()))) { return false; } } } return true; } @Override public boolean compatible(EntitySchema entitySchema) { if (!mappingCompatible(getRawSchema(), entitySchema.getRawSchema())) { return false; } AvroEntitySchema avroEntitySchema = (AvroEntitySchema) entitySchema; if (!avroReadWriteSchemasCompatible(schema, avroEntitySchema.getAvroSchema())) { return false; } if (!avroReadWriteSchemasCompatible(avroEntitySchema.getAvroSchema(), schema)) { return false; } return true; } /** * Ensure that the field mappings haven't changed between the oldSchemaString * and the newSchemaString. * * @param oldSchemaString * @param newSchemaString * @return true if the mappings are compatible, false if not. */ private static boolean mappingCompatible(String oldSchemaString, String newSchemaString) { ObjectMapper mapper = new ObjectMapper(); JsonNode oldSchema; JsonNode newSchema; try { oldSchema = mapper.readValue(oldSchemaString, JsonNode.class); newSchema = mapper.readValue(newSchemaString, JsonNode.class); } catch (IOException e) { throw new SchemaValidationException( "Schemas not proper JSON in mappingCompatible", e); } JsonNode oldSchemaFields = oldSchema.get("fields"); JsonNode newSchemaFields = newSchema.get("fields"); for (JsonNode oldSchemaField : oldSchemaFields) { String oldSchemaFieldName = oldSchemaField.get("name").getTextValue(); for (JsonNode newSchemaField : newSchemaFields) { if (oldSchemaFieldName .equals(newSchemaField.get("name").getTextValue())) { if (!oldSchemaField.get("mapping").equals( newSchemaField.get("mapping"))) { return false; } } } } return true; } /** * Returns true if the writer and reader schema are compatible with each * other, following the avro specification. * * @param writer * writer schema * @param reader * reader schema * @return True if compatible, false if not. */ private static boolean avroReadWriteSchemasCompatible(Schema writer, Schema reader) { Symbol rootSymbol; try { ResolvingGrammarGenerator g = new ResolvingGrammarGenerator(); rootSymbol = g.generate(writer, reader); } catch (IOException e) { throw new DatasetException("IOException while generating grammar.", e); } return !hasErrorSymbol(rootSymbol); } /** * Determine if the symbol tree has an error symbol in it. This would indicate * that the two schemas are not compatible. * * @param rootSymbol * The root symbol to traverse from to look for an error symbol. * @return true if an error symbol exists in the tree. */ private static boolean hasErrorSymbol(Symbol rootSymbol) { if (rootSymbol.production == null) { return false; } for (Symbol s : rootSymbol.production) { if (s == rootSymbol) { continue; } if (s.getClass().equals(Symbol.ErrorAction.class)) { return true; } else { if (s.production != null) { for (Symbol subSymbol : s.production) { if (hasErrorSymbol(subSymbol)) { return true; } } } } } return false; } private static boolean fieldsEqual(Field field1, FieldMapping field1Mapping, Field field2, FieldMapping field2Mapping) { // if names aren't equal, return false if (!field1.name().equals(field2.name())) { return false; } // if schemas aren't equal, return false if (!AvroUtils.avroSchemaTypesEqual(field1.schema(), field2.schema())) { return false; } // if field mappings aren't equal, return false if (!Objects.equal(field1Mapping, field2Mapping)) { return false; } // if one default value is null and the other isn't, return false if ((field1.defaultValue() != null && field2.defaultValue() == null) || (field1.defaultValue() == null && field2.defaultValue() != null)) { return false; } // if both default values are not null, and the default values are not // equal, return false if ((field1.defaultValue() != null && field2.defaultValue() != null) && !field1.defaultValue().equals(field2.defaultValue())) { return false; } // Fields are equal, return true return true; } }