/*
* 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.operations.service.delta;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.kaaproject.kaa.common.avro.GenericAvroConverter;
import org.kaaproject.kaa.common.dto.ConfigurationDto;
import org.kaaproject.kaa.common.dto.ConfigurationSchemaDto;
import org.kaaproject.kaa.common.dto.EndpointConfigurationDto;
import org.kaaproject.kaa.common.dto.EndpointGroupDto;
import org.kaaproject.kaa.common.dto.EndpointGroupStateDto;
import org.kaaproject.kaa.common.dto.EndpointProfileDto;
import org.kaaproject.kaa.common.dto.EndpointSpecificConfigurationDto;
import org.kaaproject.kaa.common.dto.EndpointUserConfigurationDto;
import org.kaaproject.kaa.common.endpoint.security.MessageEncoderDecoder;
import org.kaaproject.kaa.common.hash.EndpointObjectHash;
import org.kaaproject.kaa.server.common.Base64Util;
import org.kaaproject.kaa.server.common.core.algorithms.AvroUtils;
import org.kaaproject.kaa.server.common.core.algorithms.delta.BaseBinaryDelta;
import org.kaaproject.kaa.server.common.core.algorithms.override.OverrideAlgorithm;
import org.kaaproject.kaa.server.common.core.algorithms.override.OverrideAlgorithmFactory;
import org.kaaproject.kaa.server.common.core.algorithms.override.OverrideException;
import org.kaaproject.kaa.server.common.core.configuration.AbstractKaaData;
import org.kaaproject.kaa.server.common.core.configuration.BaseData;
import org.kaaproject.kaa.server.common.core.configuration.OverrideData;
import org.kaaproject.kaa.server.common.core.configuration.RawData;
import org.kaaproject.kaa.server.common.core.schema.BaseSchema;
import org.kaaproject.kaa.server.common.core.schema.OverrideSchema;
import org.kaaproject.kaa.server.common.core.schema.RawSchema;
import org.kaaproject.kaa.server.common.core.structure.Pair;
import org.kaaproject.kaa.server.common.dao.ConfigurationService;
import org.kaaproject.kaa.server.common.dao.EndpointService;
import org.kaaproject.kaa.server.common.dao.EndpointSpecificConfigurationService;
import org.kaaproject.kaa.server.common.dao.UserConfigurationService;
import org.kaaproject.kaa.server.operations.pojo.GetDeltaRequest;
import org.kaaproject.kaa.server.operations.pojo.GetDeltaResponse;
import org.kaaproject.kaa.server.operations.pojo.GetDeltaResponse.GetDeltaResponseType;
import org.kaaproject.kaa.server.operations.pojo.exceptions.GetDeltaException;
import org.kaaproject.kaa.server.operations.service.cache.AppVersionKey;
import org.kaaproject.kaa.server.operations.service.cache.CacheService;
import org.kaaproject.kaa.server.operations.service.cache.Computable;
import org.kaaproject.kaa.server.operations.service.cache.ConfigurationCacheEntry;
import org.kaaproject.kaa.server.operations.service.cache.DeltaCacheKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
/**
* Implementation of {@link DeltaService}. Delta calculation process is quite
* resource consuming. In order to minimize amount of delta calculations,
* certain caching logic is used.
*
* @author ashvayka
*/
@Service
public class DefaultDeltaService implements DeltaService {
private static final Logger LOG = LoggerFactory.getLogger(DefaultDeltaService.class);
private static final Comparator<EndpointGroupDto> ENDPOINT_GROUP_COMPARATOR =
new Comparator<EndpointGroupDto>() {
@Override
public int compare(EndpointGroupDto o1, EndpointGroupDto o2) {
if (o1.getWeight() < o2.getWeight()) {
return -1;
}
if (o1.getWeight() > o2.getWeight()) {
return 1;
}
return o1.getId().compareTo(o2.getId());
}
};
@Autowired
private CacheService cacheService;
@Autowired
private ConfigurationService configurationService;
@Autowired
private UserConfigurationService userConfigurationService;
@Autowired
private EndpointService endpointService;
@Autowired
private OverrideAlgorithmFactory configurationOverrideFactory;
@Autowired
private EndpointSpecificConfigurationService endpointSpecificConfigurationService;
/**
* Instantiates a new default delta service.
*/
public DefaultDeltaService() {
super();
}
@Override
public ConfigurationCacheEntry getConfiguration(String appToken, String endpointId, EndpointProfileDto profile) throws GetDeltaException {
LOG.debug("[{}][{}] Calculating new configuration", appToken, endpointId);
AppVersionKey appConfigVersionKey = new AppVersionKey(appToken, profile.getConfigurationVersion());
EndpointObjectHash userConfHash = EndpointObjectHash.fromBytes(profile
.getUserConfigurationHash());
EndpointObjectHash epsConfHash = EndpointObjectHash.fromBytes(profile
.getEpsConfigurationHash());
DeltaCacheKey deltaKey = new DeltaCacheKey(appConfigVersionKey, profile.getGroupState(), userConfHash, epsConfHash,
null, profile.isUseConfigurationRawSchema(), true);
LOG.debug("[{}][{}] Built resync delta key {}", appToken, endpointId, deltaKey);
return getDelta(endpointId, profile.getEndpointUserId(), deltaKey, profile.isUseConfigurationRawSchema());
}
/*
* (non-Javadoc)
*
* @see
* org.kaaproject.kaa.server.operations.service.delta.DeltaService#getDelta
* (org.kaaproject.kaa.server.operations.pojo.GetDeltaRequest,
* org.kaaproject.kaa.server.operations.service.delta.HistoryDelta, int)
*/
@Override
public GetDeltaResponse getDelta(GetDeltaRequest request) throws GetDeltaException {
GetDeltaResponse response;
EndpointProfileDto profile = request.getEndpointProfile();
String endpointId = "N/A";
if (LOG.isDebugEnabled() && profile != null && profile.getEndpointKeyHash() != null) {
endpointId = Base64Util.encode(profile.getEndpointKeyHash());
}
LOG.debug("[{}][{}] Processing configuration request",
request.getApplicationToken(), endpointId);
boolean resync = false;
if (request.isFirstRequest()) {
resync = true;
} else if (!request.getConfigurationHash().binaryEquals(profile.getConfigurationHash())) {
logHashMismatch(request, profile, endpointId);
resync = true;
}
if (resync) {
EndpointConfigurationDto configurationDto = cacheService.getConfByHash(
EndpointObjectHash.fromBytes(profile
.getConfigurationHash()));
response = new GetDeltaResponse(GetDeltaResponseType.CONF_RESYNC, new BaseBinaryDelta(
configurationDto.getConfiguration()));
} else {
response = new GetDeltaResponse(GetDeltaResponseType.NO_DELTA);
}
LOG.debug("[{}][{}] Processed configuration request {}",
request.getApplicationToken(), endpointId, response.getResponseType());
return response;
}
/**
* Calculate delta.
*
* @param deltaKey the delta key
* @return the delta cache entry
* @throws GetDeltaException the get delta exception
*/
private ConfigurationCacheEntry getDelta(final String endpointId,
final String userId,
DeltaCacheKey deltaKey,
boolean useConfigurationRawSchema)
throws GetDeltaException {
EndpointUserConfigurationDto userConfiguration = findLatestUserConfiguration(userId, deltaKey);
EndpointSpecificConfigurationDto epsConfig = Optional.ofNullable(endpointId)
.map(Base64Util::decode)
.flatMap(endpointSpecificConfigurationService::findActiveConfigurationByEndpointKeyHash)
.orElse(null);
EndpointObjectHash epsConfHash = Optional.ofNullable(epsConfig)
.map(EndpointSpecificConfigurationDto::getConfiguration)
.map(EndpointObjectHash::fromString)
.orElse(null);
EndpointObjectHash userConfHash = Optional.ofNullable(userConfiguration)
.map(EndpointUserConfigurationDto::getBody)
.map(EndpointObjectHash::fromString)
.orElse(null);
final DeltaCacheKey newKey;
if (userConfiguration != null || epsConfig != null) {
newKey = new DeltaCacheKey(
deltaKey.getAppConfigVersionKey(),
deltaKey.getEndpointGroups(),
userConfHash,
epsConfHash,
deltaKey.getConfHash(),
useConfigurationRawSchema,
deltaKey.isResyncOnly()
);
} else {
newKey = deltaKey;
}
ConfigurationCacheEntry deltaCacheEntry = cacheService.getDelta(
newKey, new Computable<DeltaCacheKey, ConfigurationCacheEntry>() { // NOSONAR
@Override
public ConfigurationCacheEntry compute(DeltaCacheKey deltaKey) {
try {
LOG.debug("[{}] Calculating delta for {}", endpointId, deltaKey);
AbstractKaaData<?> data;
ConfigurationSchemaDto latestConfigurationSchema =
cacheService.getConfSchemaByAppAndVersion(deltaKey.getAppConfigVersionKey());
EndpointUserConfigurationDto userConfig = findLatestUserConfiguration(userId, deltaKey);
Pair<BaseData, RawData> mergedConfiguration = getMergedConfiguration(endpointId, userConfig, epsConfig, deltaKey, latestConfigurationSchema);
if (useConfigurationRawSchema) {
data = mergedConfiguration.getV2();
} else {
data = mergedConfiguration.getV1();
}
LOG.trace("[{}] Merged configuration {}", endpointId, data.getRawData());
ConfigurationCacheEntry deltaCache = buildBaseResyncDelta(endpointId, data.getRawData(),
data.getSchema().getRawSchema(), userConfHash, epsConfHash);
if (cacheService.getConfByHash(deltaCache.getHash()) == null) {
EndpointConfigurationDto newConfiguration = new EndpointConfigurationDto();
newConfiguration.setConfiguration(deltaCache.getConfiguration());
newConfiguration.setConfigurationHash(deltaCache.getHash().getData());
cacheService.putConfiguration(deltaCache.getHash(), newConfiguration);
}
LOG.debug("[{}] Configuration hash for {} is {}", endpointId, deltaKey,
MessageEncoderDecoder.bytesToHex(deltaCache.getHash().getData()));
return deltaCache;
} catch (GetDeltaException | IOException ex) {
throw new RuntimeException(ex); // NOSONAR
}
}
});
return deltaCacheEntry;
}
private void logHashMismatch(GetDeltaRequest request,
EndpointProfileDto profile,
String endpointId) {
if (profile.getConfigurationHash() != null && LOG.isWarnEnabled()) {
String serverHash = "";
String clientHash = "";
if (profile.getConfigurationHash() != null) {
serverHash = MessageEncoderDecoder.bytesToHex(profile.getConfigurationHash());
}
if (request.getConfigurationHash() != null) {
clientHash = MessageEncoderDecoder.bytesToHex(request.getConfigurationHash().getData());
}
LOG.warn("[{}] Configuration hash mismatch! server {}, client {}",
endpointId, serverHash, clientHash);
}
}
private EndpointUserConfigurationDto findLatestUserConfiguration(final String userId,
DeltaCacheKey deltaKey) {
EndpointUserConfigurationDto userConfiguration = null;
if (userId != null) {
userConfiguration =
userConfigurationService.findUserConfigurationByUserIdAndAppTokenAndSchemaVersion(
userId,
deltaKey.getAppConfigVersionKey().getApplicationToken(),
deltaKey.getAppConfigVersionKey().getVersion());
if (userConfiguration != null) {
LOG.debug("[{}] User specific configuration found", userId);
} else {
LOG.debug("[{}] No user configuration found ", userId);
}
}
return userConfiguration;
}
private BaseData processEndpointGroups(List<EndpointGroupDto> endpointGroups,
List<ConfigurationDto> configurations,
ConfigurationSchemaDto configurationSchema)
throws OverrideException, IOException {
// create sorted map to store configurations sorted by endpoint group
// weight
// put all endpoint groups as keys into the map
Collections.sort(endpointGroups, ENDPOINT_GROUP_COMPARATOR);
List<OverrideData> overrideConfigs = new LinkedList<>();
BaseData baseConfig = null;
OverrideSchema overrideSchema = new OverrideSchema(configurationSchema.getOverrideSchema());
BaseSchema baseSchema = new BaseSchema(configurationSchema.getBaseSchema());
// put configurations into the map under corresponding endpoint group
for (EndpointGroupDto endpointGroup : endpointGroups) {
boolean endpointGroupFound = false;
for (ConfigurationDto configuration : configurations) {
if (configuration.getEndpointGroupId().equals(endpointGroup.getId())) {
if (endpointGroup.getWeight() != 0) {
overrideConfigs.add(new OverrideData(overrideSchema, configuration.getBody()));
} else {
baseConfig = new BaseData(baseSchema, configuration.getBody());
}
endpointGroupFound = true;
break;
}
}
if (!endpointGroupFound) {
LOG.debug("No Configuration found for Endpoint Group; Endpoint Group Id: {}",
endpointGroup.getId());
}
}
OverrideAlgorithm configurationMerger =
configurationOverrideFactory.createConfigurationOverrideAlgorithm();
return configurationMerger.override(baseConfig, overrideConfigs);
}
/**
* Gets the latest conf from cache.
*
* @return the latest conf from cache
*/
private Pair<BaseData, RawData> getMergedConfiguration(final String endpointId, final EndpointUserConfigurationDto userConfig,
EndpointSpecificConfigurationDto epsConfig, final DeltaCacheKey cacheKey,
ConfigurationSchemaDto latestConfigurationSchema) throws GetDeltaException {
final List<EndpointGroupStateDto> egsList = cacheKey.getEndpointGroups();
// return Pair in order to cache both calculated configuration and optimize performance
Pair<BaseData, RawData> mergedConfiguration = cacheService.getMergedConfiguration(egsList,
new Computable<List<EndpointGroupStateDto>, Pair<BaseData, RawData>>() {
@Override
public Pair<BaseData, RawData> compute(List<EndpointGroupStateDto> key) {
LOG.trace("[{}] getMergedConfiguration.compute begin", endpointId);
try {
List<EndpointGroupDto> endpointGroups = new ArrayList<>();
List<ConfigurationDto> configurations = new ArrayList<>();
ConfigurationSchemaDto configurationSchema = null;
for (EndpointGroupStateDto egs : egsList) {
EndpointGroupDto endpointGroup = null;
if (!StringUtils.isBlank(egs.getEndpointGroupId())) {
endpointGroup = endpointService.findEndpointGroupById(egs.getEndpointGroupId());
if (endpointGroup != null) {
endpointGroups.add(endpointGroup);
}
}
ConfigurationDto configuration = null;
if (!StringUtils.isBlank(egs.getConfigurationId())) {
configuration = configurationService.findConfigurationById(
egs.getConfigurationId());
if (configuration != null) {
configurations.add(configuration);
}
}
if (configurationSchema == null && configuration != null) {
configurationSchema = configurationService.findConfSchemaById(
configuration.getSchemaId());
}
}
BaseData baseData = processEndpointGroups(
endpointGroups, configurations, configurationSchema);
// converting merged base schema to raw schema
String ctlSchema = cacheService.getFlatCtlSchemaById(
latestConfigurationSchema.getCtlSchemaId());
JsonNode json = new ObjectMapper().readTree(baseData.getRawData());
AvroUtils.removeUuids(json);
RawData rawData = new RawData(new RawSchema(ctlSchema), json.toString());
return new Pair<>(baseData, rawData);
} catch (OverrideException | IOException ex) {
LOG.error("[{}] Unexpected exception occurred while merging configuration: ",
endpointId, ex);
throw new RuntimeException(ex); // NOSONAR
} finally {
LOG.trace("[{}] getMergedGroupConfiguration.compute end", endpointId);
}
}
});
if (userConfig != null) {
mergedConfiguration = mergeConfiguration(endpointId, userConfig.getBody(), latestConfigurationSchema, mergedConfiguration);
}
if (epsConfig != null) {
mergedConfiguration = mergeConfiguration(endpointId, epsConfig.getConfiguration(), latestConfigurationSchema, mergedConfiguration);
}
return mergedConfiguration;
}
private Pair<BaseData, RawData> mergeConfiguration(String endpointId, String config, ConfigurationSchemaDto configSchema,
Pair<BaseData, RawData> mergedConfiguration) throws GetDeltaException {
OverrideAlgorithm configurationMerger = configurationOverrideFactory.createConfigurationOverrideAlgorithm();
OverrideSchema overrideSchema = new OverrideSchema(configSchema.getOverrideSchema());
try {
LOG.trace("Merging group configuration with configuration: {}", config);
BaseData baseData = configurationMerger.override(mergedConfiguration.getV1(),
Collections.singletonList(new OverrideData(overrideSchema, config)));
JsonNode json = new ObjectMapper().readTree(baseData.getRawData());
AvroUtils.removeUuids(json);
RawData rawData = new RawData(new RawSchema(mergedConfiguration.getV2().getSchema().getRawSchema()), json.toString());
return new Pair<>(baseData, rawData);
} catch (OverrideException | IOException oe) {
LOG.error("[{}] Unexpected exception occurred while merging configuration: ", endpointId, oe);
throw new GetDeltaException(oe);
} finally {
LOG.trace("[{}] getMergedConfiguration.compute end", endpointId);
}
}
private ConfigurationCacheEntry buildBaseResyncDelta(String endpointId, String jsonData, String schema,
EndpointObjectHash userConfHash, EndpointObjectHash epsConfHash) throws IOException {
byte[] configuration = GenericAvroConverter.toRawData(jsonData, schema);
return new ConfigurationCacheEntry(configuration, new BaseBinaryDelta(configuration), EndpointObjectHash.fromSha1(configuration),
userConfHash, epsConfHash);
}
}