/*
* 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.control.service.sdk;
import org.apache.avro.Schema;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.compressors.CompressorInputStream;
import org.apache.commons.compress.compressors.CompressorOutputStream;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.kaaproject.kaa.avro.avrogen.StyleUtils;
import org.kaaproject.kaa.avro.avrogen.compiler.CCompiler;
import org.kaaproject.kaa.avro.avrogen.compiler.Compiler;
import org.kaaproject.kaa.common.dto.admin.SdkProfileDto;
import org.kaaproject.kaa.common.dto.file.FileData;
import org.kaaproject.kaa.server.common.Environment;
import org.kaaproject.kaa.server.common.Version;
import org.kaaproject.kaa.server.common.zk.ServerNameUtil;
import org.kaaproject.kaa.server.common.zk.gen.BootstrapNodeInfo;
import org.kaaproject.kaa.server.control.service.sdk.compress.TarEntryData;
import org.kaaproject.kaa.server.control.service.sdk.event.CEventSourcesGenerator;
import org.kaaproject.kaa.server.control.service.sdk.event.EventFamilyMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class CSdkGenerator extends SdkGenerator {
private static final Logger LOG = LoggerFactory.getLogger(CppSdkGenerator.class);
private static final String C_SDK_DIR = "sdk/c";
private static final String TEMPLATE_DIR = "sdk/c";
/**
* The KAA_SRC_FOLDER variable is also set in CMakeList.txt located in the root folder of the C
* SDK project.
*/
private static final String KAA_SRC_FOLDER = "src/kaa";
private static final String KAA_GEN_SOURCE_DIR = KAA_SRC_FOLDER + "/gen/";
private static final String C_SDK_PREFIX = "kaa-c-ep-sdk-";
private static final String KAA_SOURCE_PREFIX = "kaa";
private static final String C_HEADER_SUFFIX = ".h";
private static final String C_SOURCE_SUFFIX = ".c";
private static final String KAA_CMAKEGEN = "listfiles/CMakeGen.cmake";
private static final String KAA_DEFAULTS_HEADER = KAA_SRC_FOLDER + "/kaa_defaults.h";
private static final String LOGGING_HEADER = KAA_GEN_SOURCE_DIR + "kaa_logging_definitions.h";
private static final String PROFILE_HEADER = KAA_GEN_SOURCE_DIR + "kaa_profile_definitions.h";
private static final String CONFIGURATION_HEADER = KAA_GEN_SOURCE_DIR
+ "kaa_configuration_definitions.h";
private static final String NOTIFICATION_HEADER = KAA_GEN_SOURCE_DIR
+ "kaa_notification_definitions.h";
private static final String PROFILE_SOURCE_NAME_PATTERN = "kaa_profile_gen";
private static final String LOGGING_SOURCE_NAME_PATTERN = "kaa_logging_gen";
private static final String CONFIGURATION_SOURCE_NAME_PATTERN = "kaa_configuration_gen";
private static final String NOTIFICATION_SOURCE_NAME_PATTERN = "kaa_notification_gen";
private static final String PROFILE_NAMESPACE = KAA_SOURCE_PREFIX + "_" + "profile";
private static final String LOGGING_NAMESPACE = KAA_SOURCE_PREFIX + "_" + "logging";
private static final String CONFIGURATION_NAMESPACE = KAA_SOURCE_PREFIX + "_" + "configuration";
private static final String NOTIFICATION_NAMESPACE = KAA_SOURCE_PREFIX + "_" + "notification";
private final VelocityEngine velocityEngine;
/**
* Instantiates a new CSdkGenerator.
*/
public CSdkGenerator() {
velocityEngine = new VelocityEngine();
velocityEngine.addProperty("resource.loader", "class, file");
velocityEngine.addProperty("class.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
velocityEngine.addProperty("file.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.FileResourceLoader");
velocityEngine.addProperty("file.resource.loader.path", "/, .");
velocityEngine.setProperty("runtime.references.strict", true);
velocityEngine.setProperty("runtime.log.logsystem.class",
"org.apache.velocity.runtime.log.NullLogSystem");
}
/* (non-Javadoc)
* @see org.kaaproject.kaa.server.control.service.sdk.SdkGenerator#generateSdk(
* java.lang.String, java.util.List,
* java.lang.String, int,
* int, int,
* java.lang.String, java.lang.String,
* java.lang.String, byte[], java.util.List)
*/
@Override
public FileData generateSdk(String buildVersion,
List<BootstrapNodeInfo> bootstrapNodes,
SdkProfileDto sdkProfile,
String profileSchemaBody,
String notificationSchemaBody,
String configurationProtocolSchemaBody,
String configurationBaseSchemaBody,
byte[] defaultConfigurationData,
List<EventFamilyMetadata> eventFamilies,
String logSchemaBody) throws Exception {
String sdkToken = sdkProfile.getToken();
Integer profileSchemaVersion = sdkProfile.getProfileSchemaVersion();
String defaultVerifierToken = sdkProfile.getDefaultVerifierToken();
String sdkTemplateLocation = Environment.getServerHomeDir() + "/" + C_SDK_DIR + "/"
+ C_SDK_PREFIX + buildVersion + ".tar.gz";
LOG.debug("Lookup C SDK template: {}", sdkTemplateLocation);
CompressorStreamFactory csf = new CompressorStreamFactory();
ArchiveStreamFactory asf = new ArchiveStreamFactory();
CompressorInputStream cis = csf.createCompressorInputStream(CompressorStreamFactory.GZIP,
new FileInputStream(sdkTemplateLocation));
final ArchiveInputStream templateArchive = asf.createArchiveInputStream(
ArchiveStreamFactory.TAR, cis
);
ByteArrayOutputStream sdkOutput = new ByteArrayOutputStream();
CompressorOutputStream cos = csf.createCompressorOutputStream(
CompressorStreamFactory.GZIP, sdkOutput);
ArchiveOutputStream sdkFile = asf.createArchiveOutputStream(ArchiveStreamFactory.TAR, cos);
Map<String, TarEntryData> replacementData = new HashMap<>();
List<TarEntryData> sources = new ArrayList<>();
if (!StringUtils.isBlank(profileSchemaBody)) {
sources.addAll(generateProfileSources(profileSchemaBody));
}
if (!StringUtils.isBlank(logSchemaBody)) {
sources.addAll(generateLogSources(logSchemaBody));
}
if (!StringUtils.isBlank(configurationBaseSchemaBody)) {
sources.addAll(generateConfigurationSources(configurationBaseSchemaBody));
}
if (!StringUtils.isBlank(notificationSchemaBody)) {
sources.addAll(generateNotificationSources(notificationSchemaBody));
}
if (eventFamilies != null && !eventFamilies.isEmpty()) {
sources.addAll(CEventSourcesGenerator.generateEventSources(eventFamilies));
}
for (TarEntryData entryData : sources) {
replacementData.put(entryData.getEntry().getName(), entryData);
}
ArchiveEntry archiveEntry;
while ((archiveEntry = templateArchive.getNextEntry()) != null) {
if (!archiveEntry.isDirectory()) {
if (archiveEntry.getName().equals(KAA_DEFAULTS_HEADER)) {
// TODO: eliminate schema versions and substitute them for a single sdkToken
byte[] kaaDefaultsData = generateKaaDefaults(bootstrapNodes, sdkToken,
profileSchemaVersion,
configurationProtocolSchemaBody,
defaultConfigurationData,
defaultVerifierToken);
TarArchiveEntry kaaDefaultsEntry = new TarArchiveEntry(KAA_DEFAULTS_HEADER);
kaaDefaultsEntry.setSize(kaaDefaultsData.length);
sdkFile.putArchiveEntry(kaaDefaultsEntry);
sdkFile.write(kaaDefaultsData);
} else if (archiveEntry.getName().equals(KAA_CMAKEGEN)) {
// Ignore duplicate source names
List<String> sourceNames = new LinkedList<>();
for (TarEntryData sourceEntry : sources) {
String fileName = sourceEntry.getEntry().getName();
if (fileName.endsWith(C_SOURCE_SUFFIX) && !sourceNames.contains(fileName)) {
sourceNames.add(fileName);
}
}
VelocityContext context = new VelocityContext();
context.put("sourceNames", sourceNames);
String sourceData = generateSourceFromTemplate(TEMPLATE_DIR
+ File.separator + "CMakeGen.vm", context);
TarArchiveEntry kaaCMakeEntry = new TarArchiveEntry(KAA_CMAKEGEN);
kaaCMakeEntry.setSize(sourceData.length());
sdkFile.putArchiveEntry(kaaCMakeEntry);
sdkFile.write(sourceData.getBytes());
} else if (replacementData.containsKey(archiveEntry.getName())) {
TarEntryData entryData = replacementData.remove(archiveEntry.getName());
sdkFile.putArchiveEntry(entryData.getEntry());
sdkFile.write(entryData.getData());
} else {
sdkFile.putArchiveEntry(archiveEntry);
IOUtils.copy(templateArchive, sdkFile);
}
} else {
sdkFile.putArchiveEntry(archiveEntry);
}
sdkFile.closeArchiveEntry();
}
templateArchive.close();
for (String entryName : replacementData.keySet()) {
TarEntryData entryData = replacementData.get(entryName);
sdkFile.putArchiveEntry(entryData.getEntry());
sdkFile.write(entryData.getData());
sdkFile.closeArchiveEntry();
}
sdkFile.finish();
sdkFile.close();
String sdkFileName = C_SDK_PREFIX + sdkProfile.getToken() + ".tar.gz";
FileData sdk = new FileData();
sdk.setFileName(sdkFileName);
sdk.setFileData(sdkOutput.toByteArray());
return sdk;
}
private List<TarEntryData> generateSourcesFromSchema(Schema schema, String sourceName,
String namespace) {
List<TarEntryData> tarEntries = new LinkedList<>();
try (
OutputStream headerStream = new ByteArrayOutputStream();
OutputStream sourceStream = new ByteArrayOutputStream()
) {
Compiler compiler = new CCompiler(schema, sourceName, headerStream, sourceStream);
compiler.setNamespacePrefix(namespace);
compiler.generate();
tarEntries.add(createTarEntry(KAA_GEN_SOURCE_DIR + sourceName + C_HEADER_SUFFIX,
headerStream.toString()));
tarEntries.add(createTarEntry(KAA_GEN_SOURCE_DIR + sourceName + C_SOURCE_SUFFIX,
sourceStream.toString()));
} catch (Exception ex) {
LOG.error("Failed to generate C sdk sources", ex);
}
return tarEntries;
}
private String generateSourceFromTemplate(String templateFullName, VelocityContext context) {
StringWriter writer = new StringWriter();
velocityEngine.getTemplate(templateFullName).merge(context, writer);
return writer.toString();
}
private String processHeaderTemplate(String templateName, Schema schema, String namespace) {
VelocityContext context = new VelocityContext();
context.put("record_name", StyleUtils.toLowerUnderScore(schema.getName()));
context.put("namespace", namespace);
return generateSourceFromTemplate(TEMPLATE_DIR + File.separator + templateName, context);
}
private TarEntryData createTarEntry(String tarEntryName, String data) {
TarArchiveEntry entry = new TarArchiveEntry(tarEntryName);
entry.setSize(data.length());
return new TarEntryData(entry, data.getBytes());
}
/**
* Generate client properties.
*
* @param bootstrapNodes the bootstrap nodes
* @param sdkToken the sdk token
* @param profileSchemaVersion the profile schema version
* @param configurationProtocolSchemaBody the configuration protocol schema body
* @param defaultConfigurationData the default configuration data
* @return the byte[]
* @throws IOException Signals that an I/O exception has occurred.
*/
private byte[] generateKaaDefaults(List<BootstrapNodeInfo> bootstrapNodes,
String sdkToken,
int profileSchemaVersion,
String configurationProtocolSchemaBody,
byte[] defaultConfigurationData,
String defaultVerifierToken) throws IOException {
VelocityContext context = new VelocityContext();
LOG.debug("[sdk generateClientProperties] bootstrapNodes.size(): {}", bootstrapNodes.size());
context.put("build_version", Version.PROJECT_VERSION);
context.put("build_commit_hash", Version.COMMIT_HASH);
context.put("sdk_token", sdkToken);
context.put("profile_version", profileSchemaVersion);
context.put("user_verifier_token", (defaultVerifierToken != null ? defaultVerifierToken : ""));
context.put("bootstrapNodes", bootstrapNodes);
context.put("configurationData", defaultConfigurationData);
context.put("Base64", Base64.class);
context.put("Integer", Integer.class);
context.put("ServerNameUtil", ServerNameUtil.class);
return generateSourceFromTemplate(TEMPLATE_DIR + File.separator
+ "kaa_defaults.hvm", context).getBytes();
}
private List<TarEntryData> generateProfileSources(String profileSchemaBody) {
Schema schema = new Schema.Parser().parse(profileSchemaBody);
List<TarEntryData> tarEntries = new LinkedList<>();
tarEntries.add(createTarEntry(PROFILE_HEADER,
processHeaderTemplate("kaa_profile_definitions.hvm", schema, PROFILE_NAMESPACE)));
tarEntries.addAll(generateSourcesFromSchema(schema, PROFILE_SOURCE_NAME_PATTERN,
PROFILE_NAMESPACE));
return tarEntries;
}
private List<TarEntryData> generateLogSources(String logSchemaBody) {
Schema schema = new Schema.Parser().parse(logSchemaBody);
List<TarEntryData> tarEntries = new LinkedList<>();
tarEntries.add(createTarEntry(LOGGING_HEADER,
processHeaderTemplate("kaa_logging_definitions.hvm", schema, LOGGING_NAMESPACE)));
tarEntries.addAll(generateSourcesFromSchema(schema, LOGGING_SOURCE_NAME_PATTERN,
LOGGING_NAMESPACE));
return tarEntries;
}
private List<TarEntryData> generateConfigurationSources(String configurationSchemaBody) {
Schema schema = new Schema.Parser().parse(configurationSchemaBody);
List<TarEntryData> tarEntries = new LinkedList<>();
tarEntries.add(
createTarEntry(
CONFIGURATION_HEADER,
processHeaderTemplate("kaa_configuration_definitions.hvm", schema,
CONFIGURATION_NAMESPACE)
)
);
tarEntries.addAll(generateSourcesFromSchema(schema,
CONFIGURATION_SOURCE_NAME_PATTERN, CONFIGURATION_NAMESPACE));
return tarEntries;
}
private List<TarEntryData> generateNotificationSources(String notificationSchemaBody) {
Schema schema = new Schema.Parser().parse(notificationSchemaBody);
List<TarEntryData> tarEntries = new LinkedList<>();
tarEntries.add(createTarEntry(NOTIFICATION_HEADER,
processHeaderTemplate("kaa_notification_definitions.hvm", schema, NOTIFICATION_NAMESPACE)));
tarEntries.addAll(generateSourcesFromSchema(schema,
NOTIFICATION_SOURCE_NAME_PATTERN, NOTIFICATION_NAMESPACE));
return tarEntries;
}
}