/*
* 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.admin.services.schema;
import org.apache.avro.Schema;
import org.apache.avro.SchemaBuilder;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
import org.kaaproject.avro.ui.shared.Fqn;
import org.kaaproject.avro.ui.shared.FqnVersion;
import org.kaaproject.kaa.common.dto.ctl.CTLSchemaDto;
import org.kaaproject.kaa.common.dto.ctl.CtlSchemaMetaInfoDto;
import org.kaaproject.kaa.server.admin.services.util.Utils;
import org.kaaproject.kaa.server.admin.shared.services.KaaAdminServiceException;
import org.kaaproject.kaa.server.control.service.ControlService;
import org.kaaproject.kaa.server.control.service.exception.ControlServiceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This class is used to parse and validate CTL schemas on save.
*
* @author Bohdan Khablenko
* @see #parse(String, String)
* @see #validate(CTLSchemaDto)
* @since v0.8.0
*/
public class CtlSchemaParser {
private static final String VERSION = "version";
private static final String NAME = "name";
private static final String NAMESPACE = "namespace";
private static final String TYPE = "type";
private static final String FQN = "fqn";
private static final String DEPENDENCIES = "dependencies";
/**
* The Constant LOG.
*/
private static final Logger LOG = LoggerFactory.getLogger(CtlSchemaParser.class);
private final Schema.Parser parser = new Schema.Parser();
private final ControlService controlService;
private final String tenantId;
public CtlSchemaParser(ControlService controlService, String tenantId) {
this.controlService = controlService;
this.tenantId = tenantId;
}
/**
* Parses the given string CTL schema along with its dependencies as an
* {@link org.apache.avro.Schema Avro schema}.
*
* @param avroSchema A string CTL schema to parse
* @return A parsed CTL schema as an Avro schema
* @throws Exception - if the given CTL schema is invalid and thus cannot be parsed.
*/
public static Schema parseStringCtlSchema(String avroSchema) throws Exception {
Schema.Parser parser = new Schema.Parser();
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(avroSchema);
JsonNode dependenciesNode = node.get(DEPENDENCIES);
if (dependenciesNode != null && dependenciesNode.isArray()) {
Map<String, Schema> types = new HashMap<>();
for (int i = 0; i < dependenciesNode.size(); i++) {
JsonNode dependencyNode = dependenciesNode.get(i);
Fqn fqn = new Fqn(dependencyNode.get(FQN).asText());
Schema fakeSchema = SchemaBuilder
.record(fqn.getName()).namespace(fqn.getNamespace())
.fields()
.endRecord();
types.put(fqn.getFqnString(), fakeSchema);
}
parser.addTypes(types);
}
return parser.parse(avroSchema);
}
/**
* Parse CTL schema from string and return <code>CTLSchemaDto</code> instance.
*
* @param body is body of CTL schema
* @param applicationId is application id
* @return <code>CTLSchemaDto</code> instance
*/
public CTLSchemaDto parse(String body, String applicationId)
throws ControlServiceException, JsonParseException, JsonMappingException, IOException {
CtlSchemaMetaInfoDto metaInfo = new CtlSchemaMetaInfoDto();
String fqn = null;
ObjectNode object = new ObjectMapper().readValue(body, ObjectNode.class);
if (!object.has(TYPE) || !object.get(TYPE).isTextual()
|| !object.get(TYPE).getTextValue().equals("record")) {
throw new IllegalArgumentException("The data provided is not a record!");
}
if (!object.has(NAMESPACE) || !object.get(NAMESPACE).isTextual()) {
throw new IllegalArgumentException("No namespace specified!");
} else if (!object.has(NAME) || !object.get(NAME).isTextual()) {
throw new IllegalArgumentException("No name specified!");
} else {
fqn = object.get(NAMESPACE).getTextValue() + "." + object.get(NAME).getTextValue();
}
metaInfo = new CtlSchemaMetaInfoDto(fqn, tenantId, applicationId);
CTLSchemaDto schema = new CTLSchemaDto();
schema.setMetaInfo(metaInfo);
if (!object.has(VERSION) || !object.get(VERSION).isInt()) {
object.put(VERSION, 1);
}
schema.setVersion(object.get(VERSION).asInt());
Set<CTLSchemaDto> dependencies = new HashSet<>();
List<FqnVersion> missingDependencies = new ArrayList<>();
if (!object.has(DEPENDENCIES)) {
schema.setDependencySet(dependencies);
} else if (!object.get(DEPENDENCIES).isArray()) {
throw new IllegalArgumentException("Illegal dependencies format!");
} else {
for (JsonNode child : object.get(DEPENDENCIES)) {
if (!child.isObject() || !child.has(FQN) || !child.get(FQN).isTextual()
|| !child.has(VERSION) || !child.get(VERSION).isInt()) {
throw new IllegalArgumentException("Illegal dependency format!");
} else {
String dependencyFqn = child.get(FQN).asText();
int dependencyVersion = child.get(VERSION).asInt();
CTLSchemaDto dependency =
controlService.getAnyCtlSchemaByFqnVersionTenantIdAndApplicationId(
dependencyFqn, dependencyVersion, tenantId, applicationId);
if (dependency != null) {
dependencies.add(dependency);
} else {
missingDependencies.add(new FqnVersion(dependencyFqn, dependencyVersion));
}
}
}
if (!missingDependencies.isEmpty()) {
String message = "The following dependencies are missing from the database: "
+ Arrays.toString(missingDependencies.toArray());
throw new IllegalArgumentException(message);
}
schema.setDependencySet(dependencies);
}
body = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(object);
schema.setBody(body);
return schema;
}
/**
* Parses the given CTL schema along with its dependencies as an
* {@link org.apache.avro.Schema Avro schema}.
*
* @param schema A CTL schema to parse
* @return A parsed CTL schema as an Avro schema
* @throws KaaAdminServiceException - if the given CTL schema is invalid and thus cannot be
* parsed.
*/
public Schema validate(CTLSchemaDto schema) throws KaaAdminServiceException {
if (schema.getDependencySet() != null) {
for (CTLSchemaDto dependency : schema.getDependencySet()) {
try {
CTLSchemaDto dependencySchema =
controlService.getCtlSchemaByFqnVersionTenantIdAndApplicationId(
dependency.getMetaInfo().getFqn(), dependency.getVersion(),
dependency.getMetaInfo().getTenantId(),
dependency.getMetaInfo().getApplicationId());
if (dependencySchema == null) {
String message = "Unable to locate dependency \"" + dependency.getMetaInfo().getFqn()
+ "\" (version " + dependency.getVersion() + ")";
throw new IllegalArgumentException(message);
}
validate(dependencySchema);
} catch (Exception cause) {
throw Utils.handleException(cause);
}
}
}
try {
/*
* Parsed schemas are automatically added to the set of types known
* to the parser.
*/
return parser.parse(schema.getBody());
} catch (Exception cause) {
LOG.error("Unable to parse CTL schema \""
+ schema.getMetaInfo().getFqn() + "\" (version " + schema.getVersion() + "): ", cause);
throw new IllegalArgumentException("Unable to parse CTL schema \""
+ schema.getMetaInfo().getFqn() + "\" (version "
+ schema.getVersion() + "): " + cause.getMessage());
}
}
}