/* * 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.generation; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.BY_DEFAULT_FIELD; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_FIELD; import org.apache.avro.Schema; import org.apache.avro.Schema.Field; import org.apache.avro.Schema.Type; 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.codehaus.jackson.JsonNode; import org.kaaproject.kaa.common.avro.GenericAvroConverter; import org.kaaproject.kaa.server.common.core.algorithms.AvroUtils; import org.kaaproject.kaa.server.common.core.configuration.KaaData; import org.kaaproject.kaa.server.common.core.configuration.KaaDataFactory; import org.kaaproject.kaa.server.common.core.schema.KaaSchema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Default implementation of * {@link * org.kaaproject.kaa.server.common.core.algorithms.generation.DefaultRecordGenerationAlgorithm} */ public class DefaultRecordGenerationAlgorithmImpl<U extends KaaSchema, T extends KaaData<U>> implements DefaultRecordGenerationAlgorithm<T> { /** * The Constant LOG. */ private static final Logger LOG = LoggerFactory.getLogger( DefaultRecordGenerationAlgorithmImpl.class); /** * The processed types. */ private final Map<String, GenericRecord> processedTypes = new HashMap<>(); /** * The avro schema parser. */ private final Schema.Parser avroSchemaParser; /** * The avro base schema. */ private final Schema avroBaseSchema; /** * The data factory. */ private final KaaDataFactory<U, T> dataFactory; /** * The root schema. */ private final U rootSchema; /** * Instantiates a new default configuration processor. * * @param kaaSchema the base schema * @throws ConfigurationGenerationException the configuration processing exception */ public DefaultRecordGenerationAlgorithmImpl(U kaaSchema, KaaDataFactory<U, T> factory) throws ConfigurationGenerationException { LOG.debug("Generating default configuration for configuration schema: " + kaaSchema.getRawSchema()); this.rootSchema = kaaSchema; this.dataFactory = factory; this.avroSchemaParser = new Schema.Parser(); this.avroBaseSchema = this.avroSchemaParser.parse(kaaSchema.getRawSchema()); } /** * Applies the default value. * * @param schemaNode the schema node. * @param byDefault the default value. * @return generated value. * @throws ConfigurationGenerationException the configuration processing exception. */ private Object applyDefaultValue(Schema schemaNode, JsonNode byDefault) throws ConfigurationGenerationException { if (byDefault.isArray() && AvroUtils.getSchemaByType(schemaNode, Type.BYTES) != null) { // if this is a 'bytes' type then convert json bytes array to // avro 'bytes' representation or // if this is a named type - look for already processed types // or throw an exception because "by_default" is missed ByteBuffer byteBuffer = ByteBuffer.allocate(byDefault.size()); for (JsonNode oneByte : byDefault) { byteBuffer.put((byte) oneByte.asInt()); } byteBuffer.flip(); return byteBuffer; } if (byDefault.isBoolean() && AvroUtils.getSchemaByType(schemaNode, Type.BOOLEAN) != null) { return byDefault.asBoolean(); } if (byDefault.isDouble()) { if (AvroUtils.getSchemaByType(schemaNode, Type.DOUBLE) != null) { return byDefault.asDouble(); } else if (AvroUtils.getSchemaByType(schemaNode, Type.FLOAT) != null) { return (float) byDefault.asDouble(); } } if (byDefault.isIntegralNumber() && AvroUtils.getSchemaByType(schemaNode, Type.INT) != null) { return byDefault.asInt(); } if (byDefault.isIntegralNumber() && AvroUtils.getSchemaByType(schemaNode, Type.LONG) != null) { return byDefault.asLong(); } if (byDefault.isTextual()) { Schema enumSchema = AvroUtils.getSchemaByType(schemaNode, Type.ENUM); if (enumSchema != null) { String textDefaultValue = byDefault.asText(); if (enumSchema.hasEnumSymbol(textDefaultValue)) { return new GenericData.EnumSymbol(enumSchema, textDefaultValue); } } if (AvroUtils.getSchemaByType(schemaNode, Type.STRING) != null) { return byDefault.asText(); } } throw new ConfigurationGenerationException("Default value " + byDefault.toString() + " is not applicable for the field"); } /** * Processes generic type. * * @param schemaNode schema for current type. * @param byDefault the by default. * @return generated value for input type. * @throws ConfigurationGenerationException configuration processing exception */ private Object processType(Schema schemaNode, JsonNode byDefault) throws ConfigurationGenerationException { if (byDefault != null && !byDefault.isNull()) { return applyDefaultValue(schemaNode, byDefault); } if (AvroUtils.getSchemaByType(schemaNode, Type.NULL) != null) { return null; } Schema schemaToProcess = schemaNode; if (schemaToProcess.getType().equals(Type.UNION)) { schemaToProcess = schemaToProcess.getTypes().get(0); } switch (schemaToProcess.getType()) { case ARRAY: // if this an array type then return empty array instance return processArray(); case RECORD: return processRecord(schemaToProcess); case FIXED: return processFixed(schemaToProcess); case ENUM: return processEnum(schemaToProcess); case BYTES: return ByteBuffer.allocate(0).flip(); case MAP: throw new ConfigurationGenerationException("Map is not supported."); case INT: return new Integer(0); case BOOLEAN: return Boolean.FALSE; case DOUBLE: return new Double(0.0); case LONG: return new Long(0); case STRING: return new String(""); case FLOAT: return new Float(0.0); default: return null; } } /** * Processes record type. * * @param schemaNode schema for current type. * @return generated value for input record type. * @throws ConfigurationGenerationException configuration processing exception */ private Object processRecord(Schema schemaNode) throws ConfigurationGenerationException { GenericRecord result = new GenericData.Record(schemaNode); processedTypes.put(schemaNode.getFullName(), result); // process each field List<Field> fields = schemaNode.getFields(); for (Field field : fields) { Object processFieldResult = processField(field); if (processFieldResult != null) { result.put(field.name(), processFieldResult); } } return result; } /** * Processes array type. * * @return generated value for input array type. */ private Object processArray() { Schema elementTypeSchema = Schema.create(Type.NULL); return new GenericData.Array<>(0, Schema.createArray(elementTypeSchema)); } /** * Processes enum type. * * @param schemaNode schema for current type. * @return generated value for input enum type. */ private Object processEnum(Schema schemaNode) { GenericEnumSymbol result = new GenericData.EnumSymbol(schemaNode, schemaNode.getEnumSymbols().get(0)); return result; } /** * Processes fixed type. * * @param schemaNode schema for current type. * @return generated value for input record type. */ private Object processFixed(Schema schemaNode) { int size = schemaNode.getFixedSize(); byte[] bytes = new byte[size]; for (int i = 0; i < size; i++) { bytes[i] = (byte) 0; } GenericFixed result = new GenericData.Fixed(schemaNode, bytes); return result; } /** * Process field of a record type. * * @param fieldDefinition schema for field. * @return generated value for field based on its definition. * @throws ConfigurationGenerationException configuration processing exception */ private Object processField(Field fieldDefinition) throws ConfigurationGenerationException { // if this a "uuid" type then generate it if (UUID_FIELD.equals(fieldDefinition.name())) { return AvroUtils.generateUuidObject(); } return processType(fieldDefinition.schema(), fieldDefinition.getJsonProp(BY_DEFAULT_FIELD)); } /* (non-Javadoc) */ @Override public final GenericRecord getRootConfiguration() throws ConfigurationGenerationException { return getConfigurationByName(avroBaseSchema.getName(), avroBaseSchema.getNamespace()); } /* (non-Javadoc) */ @Override public final T getRootData() throws IOException, ConfigurationGenerationException { GenericRecord root = getRootConfiguration(); GenericAvroConverter<GenericRecord> converter = new GenericAvroConverter<>(root.getSchema()); try { return dataFactory.createData(rootSchema, converter.encodeToJson(root)); } catch (RuntimeException ex) { // NPE is thrown if "null" was written into a field that is not nullable // CGE is thrown if value of wrong type was written into a field LOG.error("Unexpected exception occurred while generating configuration.", ex); throw new ConfigurationGenerationException(ex); } } @Override public final GenericRecord getConfigurationByName(String name, String namespace) throws ConfigurationGenerationException { if (name == null || namespace == null) { return null; } if (processedTypes.containsKey(namespace + "." + name)) { return processedTypes.get(namespace + "." + name); } Schema schema = avroSchemaParser.getTypes().get(namespace + "." + name); if (schema != null) { return (GenericRecord) processType(schema, null); } return null; } }