/*
* 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.schema;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.DISPLAY_NAME_FIELD;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.FIELD_ACCESS_FIELD;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.FIELD_ACCESS_READ_ONLY;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.KAA_NAMESPACE;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.RESET;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UNCHANGED;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_FIELD;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_FIELD_DISPLAY_NAME;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_SIZE;
import static org.kaaproject.kaa.server.common.core.algorithms.CommonConstants.UUID_TYPE;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Field;
import org.apache.avro.Schema.Type;
import org.kaaproject.kaa.server.common.core.algorithms.AvroUtils;
import org.kaaproject.kaa.server.common.core.schema.DataSchema;
import org.kaaproject.kaa.server.common.core.schema.KaaSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Default implementation of {@link SchemaCreator}.
*
* @author Yaroslav Zeygerman
*/
public class SchemaCreatorImpl<T extends KaaSchema> implements SchemaCreator<T> {
/**
* The Constant LOG.
*/
private static final Logger LOG = LoggerFactory
.getLogger(SchemaCreatorImpl.class);
private static final String ADDRESSABLE_NAME = "addressable";
private static final String OPTIONAL_NAME = "optional";
private final Set<Schema> addressableRecords = new HashSet<Schema>();
private final HashMap<String, Schema> processedRecords = new HashMap<String, Schema>();
private final SchemaCreationStrategy<T> strategy;
private Schema uuidTSchema;
private Schema resetTSchema;
private Schema unchangedTSchema;
private Field uuidField;
private String rootSchemaName;
public SchemaCreatorImpl(SchemaCreationStrategy<T> strategy) {
this.strategy = strategy;
}
private Schema getUuidType() {
if (uuidTSchema == null) {
uuidTSchema = Schema.createFixed(UUID_TYPE, null, KAA_NAMESPACE, UUID_SIZE);
}
return uuidTSchema;
}
private Field getUuidField() {
if (uuidField == null) {
Schema uuidFieldSchema = null;
if (strategy.isUuidOptional()) {
List<Schema> union = new ArrayList<Schema>(2);
union.add(getUuidType());
union.add(Schema.create(Type.NULL));
uuidFieldSchema = Schema.createUnion(union);
} else {
uuidFieldSchema = getUuidType();
}
uuidField = new Field(UUID_FIELD, uuidFieldSchema, null, null);
uuidField.addProp(DISPLAY_NAME_FIELD, UUID_FIELD_DISPLAY_NAME);
uuidField.addProp(FIELD_ACCESS_FIELD, FIELD_ACCESS_READ_ONLY);
return uuidField;
}
Field newUuidField = new Field(uuidField.name(), uuidField.schema(), null, null);
AvroUtils.copyJsonProperties(uuidField, newUuidField);
return newUuidField;
}
private Schema getResetType() {
if (resetTSchema == null) {
List<String> resetStrings = new ArrayList<String>(1);
resetStrings.add(RESET);
resetTSchema = Schema.createEnum(RESET + "T", null, KAA_NAMESPACE, resetStrings);
}
return resetTSchema;
}
private Schema getUnchangedType() {
if (unchangedTSchema == null) {
List<String> unchangedStrings = new ArrayList<String>(1);
unchangedStrings.add(UNCHANGED);
unchangedTSchema = Schema.createEnum(UNCHANGED + "T", null, KAA_NAMESPACE, unchangedStrings);
}
return unchangedTSchema;
}
private boolean isAddressableValue(Schema value) {
if (value.getType().equals(Type.RECORD)) {
if (value.getJsonProp(ADDRESSABLE_NAME) != null
&& !value.getFullName().equals(rootSchemaName)) {
return value.getJsonProp(ADDRESSABLE_NAME).asBoolean();
}
return true;
}
return false;
}
private void addResetTypeIfArray(Schema fieldType, List<Schema> union) {
if (fieldType.getType().equals(Type.ARRAY) && strategy.isArrayEditable()) {
union.add(0, getResetType());
}
}
private Schema processArray(Schema root) throws SchemaCreationException {
boolean hasAddressableItem = false;
Schema copySchema = null;
List<Schema> newItems = null;
if (root.getElementType().getType().equals(Type.UNION)) {
List<Schema> items = root.getElementType().getTypes();
newItems = new ArrayList<Schema>(items.size() + 1);
for (Schema itemIter : items) {
Schema updatedItem = itemIter;
if (AvroUtils.isComplexSchema(itemIter)) {
updatedItem = convert(itemIter);
}
newItems.add(updatedItem);
if (isAddressableValue(itemIter) && strategy.isArrayEditable()) {
hasAddressableItem = true;
}
}
} else if (strategy.isArrayEditable()) {
hasAddressableItem = isAddressableValue(root.getElementType());
}
if (hasAddressableItem) {
if (newItems == null) {
newItems = new ArrayList<Schema>();
newItems.add(convert(root.getElementType()));
}
newItems.add(getUuidType());
}
if (newItems != null) {
copySchema = Schema.createArray(Schema.createUnion(newItems));
} else {
copySchema = Schema.createArray(convert(root.getElementType()));
}
AvroUtils.copyJsonProperties(root, copySchema);
return copySchema;
}
private Schema processRecord(Schema root) throws SchemaCreationException {
if (processedRecords.containsKey(root.getFullName())) {
return processedRecords.get(root.getFullName());
}
Schema copySchema = Schema.createRecord(root.getName(), root.getDoc(), root.getNamespace(),
root.isError());
processedRecords.put(copySchema.getFullName(), copySchema);
boolean addressable = isAddressableValue(root);
List<Field> fields = root.getFields();
List<Field> newFields = new ArrayList<Field>(fields.size() + 1);
for (Field fieldIter : fields) {
boolean optional = false;
if (fieldIter.getJsonProp(OPTIONAL_NAME) != null) {
optional = fieldIter.getJsonProp(OPTIONAL_NAME).asBoolean();
}
List<Schema> newUnion = new ArrayList<Schema>();
if (AvroUtils.isComplexSchema(fieldIter.schema())) {
addResetTypeIfArray(fieldIter.schema(), newUnion);
newUnion.add(convert(fieldIter.schema()));
} else if (fieldIter.schema().getType().equals(Type.UNION)) {
List<Schema> oldUnion = fieldIter.schema().getTypes();
for (Schema unionIter : oldUnion) {
Schema newItem = unionIter;
if (AvroUtils.isComplexSchema(unionIter)) {
addResetTypeIfArray(unionIter, newUnion);
newItem = convert(unionIter);
}
newUnion.add(newItem);
}
} else {
newUnion.add(fieldIter.schema());
}
if (strategy.isUnchangedSupported()) {
newUnion.add(getUnchangedType());
}
if (optional) {
strategy.onOptionalField(newUnion);
} else {
strategy.onMandatoryField(newUnion);
}
Field newField = null;
if (newUnion.size() > 1) {
newField = new Field(fieldIter.name(), Schema.createUnion(newUnion), fieldIter.doc(),
fieldIter.defaultValue());
} else {
newField = new Field(fieldIter.name(), newUnion.get(0), fieldIter.doc(),
fieldIter.defaultValue());
}
AvroUtils.copyJsonProperties(fieldIter, newField);
newFields.add(newField);
}
if (addressable) {
// This record supports partial updates, adding "uuid" field if it is not exists already
Optional<Field> uuidOptional = newFields.stream().filter(f -> f.name().equals(UUID_FIELD))
.findFirst();
if (!uuidOptional.isPresent()) {
newFields.add(getUuidField());
}
}
AvroUtils.copyJsonProperties(root, copySchema);
copySchema.setFields(newFields);
if (addressable) {
// Adding addressable record's name to the storage
String fullName = root.getFullName();
if (!fullName.equals(rootSchemaName)) {
addressableRecords.add(copySchema);
}
}
return copySchema;
}
private Schema convert(Schema root) throws SchemaCreationException {
switch (root.getType()) {
case ARRAY:
return processArray(root);
case RECORD:
return processRecord(root);
case MAP:
throw new SchemaCreationException("Map is not supported");
default:
return root;
}
}
@Override
public T createSchema(DataSchema configSchema) throws SchemaCreationException {
addressableRecords.clear();
processedRecords.clear();
Schema avroSchema = new Schema.Parser().parse(configSchema.getRawSchema());
rootSchemaName = avroSchema.getFullName();
Schema resultSchema = convert(avroSchema);
return strategy.createSchema(strategy.onSchemaProcessed(resultSchema, addressableRecords));
}
}