/* * 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.override; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UNCHANGED; import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_FIELD; import org.apache.avro.Schema; 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.GenericRecord; import org.kaaproject.kaa.common.avro.GenericAvroConverter; import org.kaaproject.kaa.server.common.core.algorithms.generation.ConfigurationGenerationException; import org.kaaproject.kaa.server.common.core.algorithms.generation.DefaultRecordGenerationAlgorithm; import org.kaaproject.kaa.server.common.core.algorithms.generation.DefaultRecordGenerationAlgorithmImpl; import org.kaaproject.kaa.server.common.core.configuration.BaseData; import org.kaaproject.kaa.server.common.core.configuration.BaseDataFactory; import org.kaaproject.kaa.server.common.core.configuration.OverrideData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; /** * Default implementation of {@link OverrideAlgorithm}. */ public class DefaultOverrideAlgorithm implements OverrideAlgorithm { /** * The Constant LOG. */ private static final Logger LOG = LoggerFactory.getLogger(DefaultOverrideAlgorithm.class); private DefaultRecordGenerationAlgorithm confGenerator; private Schema.Parser baseSchemaParser; /* (non-Javadoc) * @see org.kaaproject.kaa.server.operations.service.delta.merge.ConfigurationMerger#merge( * List<org.kaaproject.kaa.common.dto.EndpointGroupDto>, * List<org.kaaproject.kaa.common.dto.ConfigurationDto>, * org.kaaproject.kaa.common.dto.ConfigurationSchemaDto) */ @Override public BaseData override(BaseData baseConfiguration, List<OverrideData> overrideConfigurations) throws OverrideException, IOException { LOG.debug("Merging:base configuration = {}; override = {}", baseConfiguration, overrideConfigurations); if (baseConfiguration == null) { LOG.debug("empty endpoint groups or configurations - returning empty result"); return null; } // if we have just one configuration then return it. if (overrideConfigurations == null || overrideConfigurations.isEmpty()) { return baseConfiguration; } try { confGenerator = new DefaultRecordGenerationAlgorithmImpl(baseConfiguration.getSchema(), new BaseDataFactory()); } catch (ConfigurationGenerationException ex) { throw new OverrideException(ex); } baseSchemaParser = new Schema.Parser(); Schema baseAvroSchema = baseSchemaParser.parse(baseConfiguration.getSchema().getRawSchema()); Schema.Parser overrideSchemaParser = new Schema.Parser(); Schema overrideAvroSchema = overrideSchemaParser.parse( overrideConfigurations.get(0).getSchema().getRawSchema()); LOG.info("converter: {}", baseAvroSchema.toString()); GenericAvroConverter<GenericRecord> baseConverter = new GenericAvroConverter(baseAvroSchema); GenericAvroConverter<GenericRecord> overrideConverter = new GenericAvroConverter( overrideAvroSchema); GenericRecord mergedConfiguration = baseConverter.decodeJson(baseConfiguration.getRawData()); try { ArrayOverrideStrategyResolver arrayMergeStrategyResolver = new ArrayOverrideStrategyResolver( baseSchemaParser.getTypes()); for (OverrideData entry : overrideConfigurations) { String configurationToApply = entry.getRawData(); // else execute merge LOG.debug("Override schema {}", entry.getSchema()); GenericRecord nodeToApply = overrideConverter.decodeJson(configurationToApply); LOG.info("configurationToApply: {}", nodeToApply); applyNode(mergedConfiguration, nodeToApply, arrayMergeStrategyResolver); } return new BaseData(baseConfiguration.getSchema(), baseConverter.encodeToJson(mergedConfiguration)); } catch (IOException | ConfigurationGenerationException ex) { throw new OverrideException(ex); } } private Schema getSchemaByName(String fullName) { return baseSchemaParser.getTypes().get(fullName); } /** * Apply node. * * @param destinationRoot the destination root * @param sourceRoot the source root * @param arrayMergeStrategyResolver the array merge strategy resolver * @throws OverrideException the merge exception */ private void applyNode(GenericRecord destinationRoot, GenericRecord sourceRoot, ArrayOverrideStrategyResolver arrayMergeStrategyResolver) throws OverrideException, ConfigurationGenerationException { Schema sourceRootSchema = sourceRoot.getSchema(); // iterate over each child node and try to apply it for (Schema.Field field : sourceRootSchema.getFields()) { String sourceChildname = field.name(); Object sourceChild = sourceRoot.get(field.pos()); if (sourceChild instanceof GenericEnumSymbol) { GenericEnumSymbol sourceEnum = (GenericEnumSymbol) sourceChild; // If the field's value is "unchanged" and this field is empty // in destination data we should generate the default value for it if (sourceEnum.toString().equals(UNCHANGED)) { if (destinationRoot.get(field.pos()) == null) { GenericRecord defRec = confGenerator.getConfigurationByName( sourceRootSchema.getName(), sourceRootSchema.getNamespace()); destinationRoot.put(field.pos(), defRec.get(field.pos())); } continue; } } // if they are then try to merge them // else override destination's node with source's node Object destinationChild = destinationRoot.get(field.pos()); // avro type is different - override destination's node with source's node if (sourceChild instanceof GenericRecord) { // checking schema types GenericRecord sourceRecord = (GenericRecord) sourceChild; GenericRecord destinationRecord = null; if (destinationChild instanceof GenericRecord) { GenericRecord tempRecord = (GenericRecord) destinationChild; if (tempRecord.getSchema().getFullName().equals(sourceRecord.getSchema().getFullName())) { destinationRecord = tempRecord; } } if (destinationRecord == null) { destinationRecord = new GenericData.Record( getSchemaByName(sourceRecord.getSchema().getFullName())); destinationRoot.put(field.pos(), destinationRecord); } // merge nodes applyNode(destinationRecord, sourceRecord, arrayMergeStrategyResolver); } else if (sourceChild instanceof GenericArray) { // merge array GenericArray sourceArray = (GenericArray) sourceChild; ArrayOverrideStrategy mergeStrategy = ArrayOverrideStrategy.REPLACE; if (!sourceArray.isEmpty() && destinationChild instanceof GenericArray) { GenericArray destArray = (GenericArray) destinationChild; // Checking if first elements have same type if (!destArray.isEmpty() && destArray.get(0).getClass() == sourceArray.get(0).getClass()) { boolean resolveStrategy = false; if (destArray.get(0) instanceof GenericContainer) { GenericContainer destFirst = (GenericContainer) destArray.get(0); GenericContainer sourceFirst = (GenericContainer) sourceArray.get(0); if (destFirst.getSchema().getFullName().equals( sourceFirst.getSchema().getFullName())) { resolveStrategy = true; } } else { resolveStrategy = true; } if (resolveStrategy) { mergeStrategy = arrayMergeStrategyResolver.resolve( sourceRootSchema.getName(), sourceRootSchema.getNamespace(), sourceChildname); } } } switch (mergeStrategy) { case REPLACE: if (sourceArray.getSchema().getElementType().getType() == Schema.Type.RECORD) { GenericArray destArray = new GenericData.Array<>(sourceArray.size(), sourceArray.getSchema()); for (Object item : sourceArray) { GenericRecord recordItem = (GenericRecord) item; GenericRecord destRecord = new GenericData.Record( getSchemaByName(recordItem.getSchema().getFullName())); applyNode(destRecord, recordItem, arrayMergeStrategyResolver); destArray.add(destRecord); } destinationRoot.put(sourceChildname, destArray); } else { destinationRoot.put(sourceChildname, sourceChild); } break; case APPEND: GenericArray destArray = (GenericArray) destinationChild; if (sourceArray.getSchema().getElementType().getType() == Schema.Type.RECORD) { for (Object item : sourceArray) { GenericRecord recordItem = (GenericRecord) item; GenericRecord destRecord = new GenericData.Record( getSchemaByName(recordItem.getSchema().getFullName())); applyNode(destRecord, recordItem, arrayMergeStrategyResolver); destArray.add(destRecord); } } else { destArray.addAll(sourceArray); } break; default: break; } } else if (!UUID_FIELD.equals(field.name()) || destinationRoot.get(field.pos()) == null) { // simple node is just copied to destination node destinationRoot.put(sourceChildname, sourceChild); } } } }