/* * Copyright (c) 2017 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 */ package org.eclipse.ditto.services.connectivity.messaging; import static org.eclipse.ditto.model.base.common.ConditionChecker.checkNotNull; import static org.eclipse.ditto.model.base.headers.DittoHeaderDefinition.CORRELATION_ID; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import org.eclipse.ditto.model.base.exceptions.DittoRuntimeException; import org.eclipse.ditto.model.base.headers.DittoHeaders; import org.eclipse.ditto.model.base.headers.DittoHeadersSizeChecker; import org.eclipse.ditto.model.connectivity.ConnectionId; import org.eclipse.ditto.model.connectivity.ConnectivityModelFactory; import org.eclipse.ditto.model.connectivity.MessageMappingFailedException; import org.eclipse.ditto.model.connectivity.PayloadMapping; import org.eclipse.ditto.model.connectivity.PayloadMappingDefinition; import org.eclipse.ditto.model.connectivity.Target; import org.eclipse.ditto.protocoladapter.Adaptable; import org.eclipse.ditto.protocoladapter.HeaderTranslator; import org.eclipse.ditto.protocoladapter.ProtocolAdapter; import org.eclipse.ditto.protocoladapter.ProtocolFactory; import org.eclipse.ditto.services.base.config.limits.LimitsConfig; import org.eclipse.ditto.services.connectivity.mapping.DefaultMessageMapperFactory; import org.eclipse.ditto.services.connectivity.mapping.DittoMessageMapper; import org.eclipse.ditto.services.connectivity.mapping.MessageMapper; import org.eclipse.ditto.services.connectivity.mapping.MessageMapperFactory; import org.eclipse.ditto.services.connectivity.mapping.MessageMapperRegistry; import org.eclipse.ditto.services.connectivity.messaging.config.ConnectivityConfig; import org.eclipse.ditto.services.connectivity.util.ConnectionLogUtil; import org.eclipse.ditto.services.models.connectivity.ExternalMessage; import org.eclipse.ditto.services.models.connectivity.ExternalMessageFactory; import org.eclipse.ditto.services.models.connectivity.MappedInboundExternalMessage; import org.eclipse.ditto.services.models.connectivity.OutboundSignal; import org.eclipse.ditto.services.models.connectivity.OutboundSignalFactory; import org.eclipse.ditto.services.utils.akka.logging.DittoDiagnosticLoggingAdapter; import org.eclipse.ditto.services.utils.protocol.ProtocolAdapterProvider; import org.eclipse.ditto.signals.base.Signal; import akka.actor.ActorSystem; /** * Processes incoming {@link ExternalMessage}s to {@link Signal}s and {@link Signal}s back to {@link ExternalMessage}s. * Encapsulates the message processing logic from the message mapping processor actor. */ public final class MessageMappingProcessor { private final ConnectionId connectionId; private final MessageMapperRegistry registry; private final DittoDiagnosticLoggingAdapter logger; private final ProtocolAdapter protocolAdapter; private final DittoHeadersSizeChecker dittoHeadersSizeChecker; private MessageMappingProcessor(final ConnectionId connectionId, final MessageMapperRegistry registry, final DittoDiagnosticLoggingAdapter logger, final ProtocolAdapter protocolAdapter, final DittoHeadersSizeChecker dittoHeadersSizeChecker) { this.connectionId = connectionId; this.registry = registry; this.logger = logger; this.protocolAdapter = protocolAdapter; this.dittoHeadersSizeChecker = dittoHeadersSizeChecker; } /** * Initializes a new command processor with mappers defined in mapping mappingContext. * The dynamic access is needed to instantiate message mappers for an actor system. * * @param connectionId the connection that the processor works for. * @param mappingDefinition the configured mappings used by this processor * @param actorSystem the dynamic access used for message mapper instantiation. * @param connectivityConfig the configuration settings of the Connectivity service. * @param protocolAdapterProvider provides the ProtocolAdapter to be used. * @param log the log adapter. * @return the processor instance. * @throws org.eclipse.ditto.model.connectivity.MessageMapperConfigurationInvalidException if the configuration of * one of the {@code mappingContext} is invalid. * @throws org.eclipse.ditto.model.connectivity.MessageMapperConfigurationFailedException if the configuration of * one of the {@code mappingContext} failed for a mapper specific reason. */ public static MessageMappingProcessor of(final ConnectionId connectionId, final PayloadMappingDefinition mappingDefinition, final ActorSystem actorSystem, final ConnectivityConfig connectivityConfig, final ProtocolAdapterProvider protocolAdapterProvider, final DittoDiagnosticLoggingAdapter log) { final MessageMapperFactory messageMapperFactory = DefaultMessageMapperFactory.of(connectionId, actorSystem, connectivityConfig.getMappingConfig(), log); final MessageMapperRegistry registry = messageMapperFactory.registryOf(DittoMessageMapper.CONTEXT, mappingDefinition); final LimitsConfig limitsConfig = connectivityConfig.getLimitsConfig(); final DittoHeadersSizeChecker dittoHeadersSizeChecker = DittoHeadersSizeChecker.of(limitsConfig.getHeadersMaxSize(), limitsConfig.getAuthSubjectsMaxCount()); return new MessageMappingProcessor(connectionId, registry, log, protocolAdapterProvider.getProtocolAdapter(null), dittoHeadersSizeChecker); } /** * @return the message mapper registry to use for mapping messages. */ MessageMapperRegistry getRegistry() { return registry; } /** * Processes an {@link ExternalMessage} which may result in 0..n messages/errors. * * @param message the inbound {@link ExternalMessage} to be processed * @param resultHandler handles the 0..n results of the mapping(s). * @return combined results of all message mappers. * @param <R> type of results. */ <R> R process(final ExternalMessage message, final MappingResultHandler<MappedInboundExternalMessage, R> resultHandler) { ConnectionLogUtil.enhanceLogWithCorrelationIdAndConnectionId(logger, message.getHeaders().get(CORRELATION_ID.getKey()), connectionId); final List<MessageMapper> mappers = getMappers(message); logger.debug("Mappers resolved for message: {}", mappers); R result = resultHandler.emptyResult(); for (final MessageMapper mapper : mappers) { final MappingTimer mappingTimer = MappingTimer.inbound(connectionId); final R mappingResult = mappingTimer.overall(() -> convertInboundMessage(mapper, message, mappingTimer, resultHandler)); result = resultHandler.combineResults(result, mappingResult); } return result; } /** * Processes an {@link OutboundSignal} to 0..n {@link OutboundSignal.Mapped} signals and passes them to the given * {@link MappingResultHandler}. * * @param outboundSignal the outboundSignal to be processed. * @param resultHandler handles the 0..n results of the mapping(s). * @param <R> type of results. */ <R> R process(final OutboundSignal outboundSignal, final MappingResultHandler<OutboundSignal.Mapped, R> resultHandler) { final List<OutboundSignal.Mappable> mappableSignals; if (outboundSignal.getTargets().isEmpty()) { // responses/errors do not have a target assigned, read mapper used for inbound message from internal header final PayloadMapping payloadMapping = outboundSignal.getSource() .getDittoHeaders() .getInboundPayloadMapper() .map(ConnectivityModelFactory::newPayloadMapping) .orElseGet(ConnectivityModelFactory::emptyPayloadMapping); // fallback to default payload mapping final OutboundSignal.Mappable mappableSignal = OutboundSignalFactory.newMappableOutboundSignal(outboundSignal.getSource(), outboundSignal.getTargets(), payloadMapping); mappableSignals = Collections.singletonList(mappableSignal); } else { // group targets with exact same list of mappers together to avoid redundant mappings mappableSignals = outboundSignal.getTargets() .stream() .collect(Collectors.groupingBy(Target::getPayloadMapping, LinkedHashMap::new, Collectors.toList())) .entrySet() .stream() .map(e -> OutboundSignalFactory.newMappableOutboundSignal(outboundSignal.getSource(), e.getValue(), e.getKey())) .collect(Collectors.toList()); } return processMappableSignals(outboundSignal, mappableSignals, resultHandler); } private <R> R processMappableSignals(final OutboundSignal outboundSignal, final List<OutboundSignal.Mappable> mappableSignals, final MappingResultHandler<OutboundSignal.Mapped, R> resultHandler) { final MappingTimer timer = MappingTimer.outbound(connectionId); final Adaptable adaptableWithoutExtra = timer.protocol(() -> protocolAdapter.toAdaptable(outboundSignal.getSource())); final Adaptable adaptable = outboundSignal.getExtra() .map(extra -> ProtocolFactory.setExtra(adaptableWithoutExtra, extra)) .orElse(adaptableWithoutExtra); enhanceLogFromAdaptable(adaptable); resultHandler.onTopicPathResolved(adaptable.getTopicPath()); R result = resultHandler.emptyResult(); for (final OutboundSignal.Mappable mappableSignal : mappableSignals) { final Signal<?> source = mappableSignal.getSource(); final List<Target> targets = mappableSignal.getTargets(); final List<MessageMapper> mappers = getMappers(mappableSignal.getPayloadMapping()); logger.withCorrelationId(source) .debug("Resolved mappers for message {} to targets {}: {}", source, targets, mappers); // convert messages in the order of payload mapping and forward to result handler for (final MessageMapper mapper : mappers) { final R nextResult = convertOutboundMessage(mappableSignal, adaptable, mapper, timer, resultHandler); result = resultHandler.combineResults(result, nextResult); } } return result; } private List<MessageMapper> getMappers(final PayloadMapping payloadMapping) { final List<MessageMapper> mappers = registry.getMappers(payloadMapping); if (mappers.isEmpty()) { logger.debug("Falling back to default MessageMapper for mapping as no MessageMapper was present."); return Collections.singletonList(registry.getDefaultMapper()); } else { return mappers; } } /** * Retrieve the header translator of the protocol adapter. * * @return the header translator. */ HeaderTranslator getHeaderTranslator() { return protocolAdapter.headerTranslator(); } private <R> R convertInboundMessage(final MessageMapper mapper, final ExternalMessage message, final MappingTimer timer, final MappingResultHandler<MappedInboundExternalMessage, R> handler) { checkNotNull(message, "message"); R result = handler.emptyResult(); try { final boolean shouldMapMessage = message.findContentType() .map(filterByContentTypeBlacklist(mapper)) .orElse(true); // if no content-type was present, map the message! if (shouldMapMessage) { logger.withCorrelationId(message.getInternalHeaders()) .debug("Mapping message using mapper {}.", mapper.getId()); final List<Adaptable> adaptables = timer.payload(mapper.getId(), () -> mapper.map(message)); if (isNullOrEmpty(adaptables)) { return handler.onMessageDropped(); } else { for (final Adaptable adaptable : adaptables) { enhanceLogFromAdaptable(adaptable); handler.onTopicPathResolved(adaptable.getTopicPath()); final Signal<?> signal = timer.protocol(() -> protocolAdapter.fromAdaptable(adaptable)); dittoHeadersSizeChecker.check(signal.getDittoHeaders()); final DittoHeaders dittoHeaders = signal.getDittoHeaders(); final DittoHeaders headersWithMapper = dittoHeaders.toBuilder().inboundPayloadMapper(mapper.getId()).build(); final Signal<?> signalWithMapperHeader = signal.setDittoHeaders(headersWithMapper); final MappedInboundExternalMessage mappedMessage = MappedInboundExternalMessage.of(message, adaptable.getTopicPath(), signalWithMapperHeader); result = handler.combineResults(result, handler.onMessageMapped(mappedMessage)); } } } else { result = handler.onMessageDropped(); logger.withCorrelationId(message.getInternalHeaders()) .debug("Not mapping message with mapper <{}> as content-type <{}> was blacklisted.", mapper.getId(), message.findContentType()); } } catch (final DittoRuntimeException e) { // combining error result with any previously successfully mapped result result = handler.combineResults(result, handler.onException(e)); } catch (final Exception e) { final MessageMappingFailedException mappingFailedException = buildMappingFailedException("inbound", message.findContentType().orElse(""), mapper.getId(), DittoHeaders.of(message.getHeaders()), e); // combining error result with any previously successfully mapped result result = handler.combineResults(result, handler.onException(mappingFailedException)); } return result; } private static Function<String, Boolean> filterByContentTypeBlacklist(final MessageMapper mapper) { return contentType -> !mapper.getContentTypeBlacklist().contains(contentType); } private <R> R convertOutboundMessage(final OutboundSignal.Mappable outboundSignal, final Adaptable adaptable, final MessageMapper mapper, final MappingTimer timer, final MappingResultHandler<OutboundSignal.Mapped, R> resultHandler) { R result = resultHandler.emptyResult(); try { logger.withCorrelationId(adaptable) .debug("Applying mapper <{}> to message <{}>", mapper.getId(), adaptable); final List<OutboundSignal.Mapped> messages = timer.payload(mapper.getId(), () -> toStream(mapper.map(adaptable)) .map(em -> { final ExternalMessage externalMessage = ExternalMessageFactory.newExternalMessageBuilder(em) .withTopicPath(adaptable.getTopicPath()) // TODO check if same as signal.getDittoHeaders() .withInternalHeaders(adaptable.getDittoHeaders()) .build(); return OutboundSignalFactory.newMappedOutboundSignal(outboundSignal, adaptable, externalMessage); }) .collect(Collectors.toList())); logger.withCorrelationId(adaptable) .debug("Mapping <{}> produced <{}> messages.", mapper.getId(), messages.size()); if (messages.isEmpty()) { result = resultHandler.combineResults(result, resultHandler.onMessageDropped()); } else { for (final OutboundSignal.Mapped message : messages) { result = resultHandler.combineResults(result, resultHandler.onMessageMapped(message)); } } } catch (final DittoRuntimeException e) { result = resultHandler.combineResults(result, resultHandler.onException(e)); } catch (final Exception e) { final Optional<DittoHeaders> headers = adaptable.getHeaders(); final String contentType = headers.map(h -> h.get(ExternalMessage.CONTENT_TYPE_HEADER)).orElse(""); final MessageMappingFailedException mappingFailedException = buildMappingFailedException("outbound", contentType, mapper.getId(), headers.orElseGet(DittoHeaders::empty), e); result = resultHandler.combineResults(result, resultHandler.onException(mappingFailedException)); } return result; } private static <T> Stream<T> toStream(@Nullable final Collection<T> messages) { return messages == null ? Stream.empty() : messages.stream(); } private static boolean isNullOrEmpty(@Nullable final Collection<?> messages) { return messages == null || messages.isEmpty(); } private List<MessageMapper> getMappers(final ExternalMessage message) { final List<MessageMapper> mappings = message.getPayloadMapping() .map(registry::getMappers) .orElseGet(Collections::emptyList); if (mappings.isEmpty()) { logger.withCorrelationId(message.getInternalHeaders()) .debug("Falling back to Default MessageMapper for mapping ExternalMessage as no MessageMapper was" + " present: {}", message); return Collections.singletonList(registry.getDefaultMapper()); } else { return mappings; } } private void enhanceLogFromAdaptable(final Adaptable adaptable) { ConnectionLogUtil.enhanceLogWithCorrelationIdAndConnectionId(logger, adaptable, connectionId); } private static MessageMappingFailedException buildMappingFailedException(final String direction, final String contentType, final String mapperId, final DittoHeaders dittoHeaders, final Exception e) { final String description = String.format("Could not map %s message with mapper '%s' due to unknown problem: %s %s", direction, mapperId, e.getClass().getSimpleName(), e.getMessage()); return MessageMappingFailedException.newBuilder(contentType) .description(description) .dittoHeaders(dittoHeaders) .cause(e) .build(); } }