Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.eclipse.hawkbit.amqp.AmqpMessageHandlerService Maven / Gradle / Ivy
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.amqp;
import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT;
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED;
import static org.springframework.util.StringUtils.hasText;
import java.io.Serializable;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus;
import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate;
import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing;
import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.UpdateMode;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.ActionProperties;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.util.IpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.MessageConversionException;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.util.StringUtils;
/**
*
* {@link AmqpMessageHandlerService} handles all incoming target interaction
* AMQP messages (e.g. create target, check for updates etc.) for the queue
* which is configured for the property hawkbit.dmf.rabbitmq.receiverQueue.
*
*/
public class AmqpMessageHandlerService extends BaseAmqpService {
private static final Logger LOG = LoggerFactory.getLogger(AmqpMessageHandlerService.class);
private final AmqpMessageDispatcherService amqpMessageDispatcherService;
private ControllerManagement controllerManagement;
private final EntityFactory entityFactory;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final SystemSecurityContext systemSecurityContext;
private static final String THING_ID_NULL = "ThingId is null";
private static final String EMPTY_MESSAGE_BODY = "\"\"";
/**
* Constructor.
*
* @param rabbitTemplate
* for converting messages
* @param amqpMessageDispatcherService
* to sending events to DMF client
* @param controllerManagement
* for target repo access
* @param entityFactory
* to create entities
* @param systemSecurityContext
* the system Security Context
* @param tenantConfigurationManagement
* the tenant configuration Management
*/
public AmqpMessageHandlerService(final RabbitTemplate rabbitTemplate,
final AmqpMessageDispatcherService amqpMessageDispatcherService,
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
final SystemSecurityContext systemSecurityContext,
final TenantConfigurationManagement tenantConfigurationManagement) {
super(rabbitTemplate);
this.amqpMessageDispatcherService = amqpMessageDispatcherService;
this.controllerManagement = controllerManagement;
this.entityFactory = entityFactory;
this.systemSecurityContext = systemSecurityContext;
this.tenantConfigurationManagement = tenantConfigurationManagement;
}
/**
* Method to handle all incoming DMF amqp messages.
*
* @param message
* incoming message
* @param type
* the message type
* @param tenant
* the contentType of the message
* @return a message if no message is send back to sender
*/
@RabbitListener(queues = "${hawkbit.dmf.rabbitmq.receiverQueue:dmf_receiver}", containerFactory = "listenerContainerFactory")
public Message onMessage(final Message message,
@Header(name = MessageHeaderKey.TYPE, required = false) final String type,
@Header(name = MessageHeaderKey.TENANT, required = false) final String tenant) {
return onMessage(message, type, tenant, getRabbitTemplate().getConnectionFactory().getVirtualHost());
}
/**
* * Executed if a amqp message arrives.
*
* @param message
* the message
* @param type
* the type
* @param tenant
* the tenant
* @param virtualHost
* the virtual host
* @return the rpc message back to supplier.
*/
public Message onMessage(final Message message, final String type, final String tenant, final String virtualHost) {
if (StringUtils.isEmpty(type) || StringUtils.isEmpty(tenant)) {
throw new AmqpRejectAndDontRequeueException("Invalid message! tenant and type header are mandatory!");
}
final SecurityContext oldContext = SecurityContextHolder.getContext();
try {
final MessageType messageType = MessageType.valueOf(type);
switch (messageType) {
case THING_CREATED:
setTenantSecurityContext(tenant);
registerTarget(message, virtualHost);
break;
case THING_REMOVED:
setTenantSecurityContext(tenant);
deleteTarget(message);
break;
case EVENT:
checkContentTypeJson(message);
setTenantSecurityContext(tenant);
handleIncomingEvent(message);
break;
case PING:
if (isCorrelationIdNotEmpty(message)) {
amqpMessageDispatcherService.sendPingReponseToDmfReceiver(message, tenant, virtualHost);
}
break;
default:
logAndThrowMessageError(message, "No handle method was found for the given message type.");
}
} catch (final IllegalArgumentException ex) {
throw new AmqpRejectAndDontRequeueException("Invalid message!", ex);
} finally {
SecurityContextHolder.setContext(oldContext);
}
return null;
}
private static void setSecurityContext(final Authentication authentication) {
final SecurityContextImpl securityContextImpl = new SecurityContextImpl();
securityContextImpl.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContextImpl);
}
private static void setTenantSecurityContext(final String tenantId) {
final AnonymousAuthenticationToken authenticationToken = new AnonymousAuthenticationToken(
UUID.randomUUID().toString(), "AMQP-Controller",
Collections.singletonList(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS)));
authenticationToken.setDetails(new TenantAwareAuthenticationDetails(tenantId, true));
setSecurityContext(authenticationToken);
}
/**
* Method to create a new target or to find the target if it already exists and
* update its poll time, status and optionally its name.
*
* @param message
* the message that contains replyTo property and optionally the name
* in body
* @param virtualHost
* the virtual host
*/
private void registerTarget(final Message message, final String virtualHost) {
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
final String replyTo = message.getMessageProperties().getReplyTo();
if (StringUtils.isEmpty(replyTo)) {
logAndThrowMessageError(message, "No ReplyTo was set for the createThing message.");
}
try {
final URI amqpUri = IpUtil.createAmqpUri(virtualHost, replyTo);
final Target target;
if (isOptionalMessageBodyEmpty(message)) {
target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri);
} else {
checkContentTypeJson(message);
target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri, convertMessage(message, DmfCreateThing.class).getName());
}
LOG.debug("Target {} reported online state.", thingId);
sendUpdateCommandToTarget(target);
} catch (final EntityAlreadyExistsException e) {
throw new AmqpRejectAndDontRequeueException("Tried to register previously registered target, message will be ignored!", e);
}
}
private static boolean isOptionalMessageBodyEmpty(final Message message) {
// empty byte array message body is serialized to double-quoted string
// by message converter and should also be considered as empty
return isMessageBodyEmpty(message) || EMPTY_MESSAGE_BODY.equals(new String(message.getBody()));
}
private void sendUpdateCommandToTarget(final Target target) {
if (isMultiAssignmentsEnabled()) {
sendCurrentActionsAsMultiActionToTarget(target);
} else {
sendOldestActionToTarget(target);
}
}
private void sendCurrentActionsAsMultiActionToTarget(final Target target) {
final List actions = controllerManagement
.findActiveActionsWithHighestWeight(target.getControllerId(), MAX_ACTION_COUNT);
final Set distributionSets = actions.stream().map(Action::getDistributionSet)
.collect(Collectors.toSet());
final Map>> softwareModulesPerDistributionSet = distributionSets
.stream().collect(Collectors.toMap(DistributionSet::getId, this::getSoftwareModulesWithMetadata));
amqpMessageDispatcherService.sendMultiActionRequestToTarget(target.getTenant(), target, actions,
action -> softwareModulesPerDistributionSet.get(action.getDistributionSet().getId()));
}
private void sendOldestActionToTarget(final Target target) {
final Optional actionOptional = controllerManagement
.findActiveActionWithHighestWeight(target.getControllerId());
if (!actionOptional.isPresent()) {
return;
}
final Action action = actionOptional.get();
if (action.isCancelingOrCanceled()) {
amqpMessageDispatcherService.sendCancelMessageToTarget(target.getTenant(), target.getControllerId(),
action.getId(), target.getAddress());
} else {
amqpMessageDispatcherService.sendUpdateMessageToTarget(new ActionProperties(action), action.getTarget(),
getSoftwareModulesWithMetadata(action.getDistributionSet()));
}
}
private Map> getSoftwareModulesWithMetadata(
final DistributionSet distributionSet) {
final List smIds = distributionSet.getModules().stream().map(SoftwareModule::getId)
.collect(Collectors.toList());
final Map> metadata = controllerManagement
.findTargetVisibleMetaDataBySoftwareModuleId(smIds);
return distributionSet.getModules().stream()
.collect(Collectors.toMap(sm -> sm, sm -> metadata.getOrDefault(sm.getId(), Collections.emptyList())));
}
/**
* Method to handle the different topics to an event.
*
* @param message
* the incoming event message.
*/
private void handleIncomingEvent(final Message message) {
switch (EventTopic.valueOf(getStringHeaderKey(message, MessageHeaderKey.TOPIC, "EventTopic is null"))) {
case UPDATE_ACTION_STATUS:
updateActionStatus(message);
break;
case UPDATE_ATTRIBUTES:
updateAttributes(message);
break;
default:
logAndThrowMessageError(message, "Got event without appropriate topic.");
break;
}
}
private void deleteTarget(final Message message) {
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
controllerManagement.deleteExistingTarget(thingId);
}
private void updateAttributes(final Message message) {
final DmfAttributeUpdate attributeUpdate = convertMessage(message, DmfAttributeUpdate.class);
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
controllerManagement.updateControllerAttributes(thingId, attributeUpdate.getAttributes(),
getUpdateMode(attributeUpdate));
}
/**
* Method to update the action status of an action through the event.
*
* @param message
* the object form the ampq message
*/
private void updateActionStatus(final Message message) {
final DmfActionUpdateStatus actionUpdateStatus = convertMessage(message, DmfActionUpdateStatus.class);
final Action action = checkActionExist(message, actionUpdateStatus);
final List messages = actionUpdateStatus.getMessage();
if (isCorrelationIdNotEmpty(message)) {
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + "DMF message correlation-id "
+ message.getMessageProperties().getCorrelationId());
}
final Status status = mapStatus(message, actionUpdateStatus, action);
final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId()).status(status)
.messages(messages);
final Action updatedAction = (Status.CANCELED == status)
? controllerManagement.addCancelActionStatus(actionStatus)
: controllerManagement.addUpdateActionStatus(actionStatus);
if (shouldTargetProceed(updatedAction)) {
sendUpdateCommandToTarget(action.getTarget());
}
}
private static boolean shouldTargetProceed(final Action action) {
return !action.isActive() || (action.hasMaintenanceSchedule() && action.isMaintenanceWindowAvailable());
}
private static boolean isCorrelationIdNotEmpty(final Message message) {
return StringUtils.hasLength(message.getMessageProperties().getCorrelationId());
}
// Exception squid:MethodCyclomaticComplexity - false positive, is a simple
// mapping
@SuppressWarnings("squid:MethodCyclomaticComplexity")
private static Status mapStatus(final Message message, final DmfActionUpdateStatus actionUpdateStatus,
final Action action) {
Status status = null;
switch (actionUpdateStatus.getActionStatus()) {
case DOWNLOAD:
status = Status.DOWNLOAD;
break;
case RETRIEVED:
status = Status.RETRIEVED;
break;
case RUNNING:
status = Status.RUNNING;
break;
case CANCELED:
status = Status.CANCELED;
break;
case FINISHED:
status = Status.FINISHED;
break;
case ERROR:
status = Status.ERROR;
break;
case WARNING:
status = Status.WARNING;
break;
case DOWNLOADED:
status = Status.DOWNLOADED;
break;
case CANCEL_REJECTED:
status = handleCancelRejectedState(message, action);
break;
default:
logAndThrowMessageError(message, "Status for action does not exisit.");
}
return status;
}
private static Status handleCancelRejectedState(final Message message, final Action action) {
if (action.isCancelingOrCanceled()) {
return Status.CANCEL_REJECTED;
}
logAndThrowMessageError(message,
"Cancel rejected message is not allowed, if action is on state: " + action.getStatus());
return null;
}
// Exception squid:S3655 - logAndThrowMessageError throws exception, i.e.
// get will not be called
@SuppressWarnings("squid:S3655")
private Action checkActionExist(final Message message, final DmfActionUpdateStatus actionUpdateStatus) {
final Long actionId = actionUpdateStatus.getActionId();
LOG.debug("Target notifies intermediate about action {} with status {}.", actionId,
actionUpdateStatus.getActionStatus());
final Optional findActionWithDetails = controllerManagement.findActionWithDetails(actionId);
if (!findActionWithDetails.isPresent()) {
logAndThrowMessageError(message,
"Got intermediate notification about action " + actionId + " but action does not exist");
}
return findActionWithDetails.get();
}
/**
* Retrieve the update mode from the given update message.
*/
private static UpdateMode getUpdateMode(final DmfAttributeUpdate update) {
final DmfUpdateMode mode = update.getMode();
if (mode != null) {
return UpdateMode.valueOf(mode.name());
}
return null;
}
private boolean isMultiAssignmentsEnabled() {
return getConfigValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class);
}
private T getConfigValue(final String key, final Class valueType) {
return systemSecurityContext
.runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(key, valueType).getValue());
}
// for testing
public void setControllerManagement(final ControllerManagement controllerManagement) {
this.controllerManagement = controllerManagement;
}
}