Skip to content

Commit

Permalink
Issue #106: Extract 'channel=live' header also for Error responses
Browse files Browse the repository at this point in the history
* the channel header is already extracted for normal messages
* now it will also be extracted for Error responses, when available
* Refactor header-extraction to reuse for normal and error responses

Signed-off-by: Joel Bartelheimer <joel.bartelheimer@bosch.io>
  • Loading branch information
jbartelh committed Nov 25, 2021
1 parent 32503db commit 7cdff17
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@

import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.ditto.base.model.exceptions.DittoJsonException;
import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.json.Jsonifiable;
import org.eclipse.ditto.json.JsonFactory;
Expand All @@ -33,6 +31,7 @@
import org.eclipse.ditto.protocol.PayloadPathMatcher;
import org.eclipse.ditto.protocol.ProtocolFactory;
import org.eclipse.ditto.protocol.TopicPath;
import org.eclipse.ditto.protocol.adapter.HeadersFromTopicPath.Extractor;
import org.eclipse.ditto.protocol.mappingstrategies.MappingStrategies;

/**
Expand Down Expand Up @@ -141,31 +140,13 @@ protected String getTypeCriterionAsString(final TopicPath topicPath) {

// filter headers by header translator, then inject any missing information from topic path
private DittoHeaders filterHeadersAndAddExtraHeadersFromTopicPath(final Adaptable externalAdaptable) {
return DittoHeaders.newBuilder(headerTranslator.fromExternalHeaders(externalAdaptable.getDittoHeaders()))
.putHeaders(getExtraHeadersFromTopicPath(externalAdaptable.getTopicPath()))
.build();
}

/**
* Add any extra information in topic path as Ditto headers.
*
* @param topicPath the topic path to extract information from.
* @return headers containing extra information from topic path.
*/
private static Map<String, String> getExtraHeadersFromTopicPath(final TopicPath topicPath) {
final Map<String, String> result = new HashMap<>();

// add entity ID for known topic-paths for error reporting.
result.put(DittoHeaderDefinition.ENTITY_ID.getKey(), getEntityId(topicPath));
if (topicPath.isChannel(TopicPath.Channel.LIVE)) {
result.put(DittoHeaderDefinition.CHANNEL.getKey(), TopicPath.Channel.LIVE.getName());
}
return result;
}
final DittoHeaders dittoHeadersFromExternal =
DittoHeaders.of(headerTranslator.fromExternalHeaders(externalAdaptable.getDittoHeaders()));

private static String getEntityId(final TopicPath topicPath) {
final TopicPath.Group group = topicPath.getGroup();
return String.join(":", group.getEntityType(), topicPath.getNamespace(), topicPath.getEntityName());
return HeadersFromTopicPath.injectHeaders(dittoHeadersFromExternal,
externalAdaptable.getTopicPath(),
Extractor::liveChannelExtractor,
Extractor::entityIdExtractor);
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.eclipse.ditto.protocol.TopicPath;
import org.eclipse.ditto.protocol.TopicPathBuildable;
import org.eclipse.ditto.protocol.TopicPathBuilder;
import org.eclipse.ditto.protocol.adapter.HeadersFromTopicPath.Extractor;
import org.eclipse.ditto.things.model.ThingConstants;

/**
Expand Down Expand Up @@ -71,10 +72,14 @@ public static DittoRuntimeException parseWithErrorRegistry(final JsonObject erro

@Override
public T fromAdaptable(final Adaptable adaptable) {
final DittoHeaders dittoHeaders = DittoHeaders.of(
headerTranslator.fromExternalHeaders(adaptable.getDittoHeaders()));
final TopicPath topicPath = adaptable.getTopicPath();

final DittoHeaders dittoHeadersFromExternal =
DittoHeaders.of(headerTranslator.fromExternalHeaders(adaptable.getDittoHeaders()));

final DittoHeaders dittoHeaders = HeadersFromTopicPath.injectHeaders(dittoHeadersFromExternal, topicPath,
Extractor::liveChannelExtractor);

final DittoRuntimeException dittoRuntimeException = adaptable.getPayload()
.getValue()
.map(JsonValue::asObject)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2021 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.protocol.adapter;

import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.protocol.TopicPath;

/**
* A function to extract information from the topic path and enrich the ditto headers with them.
*
* A set of available extractors, e.g. for live-channel and entityId, which can be used for
* {@link #injectHeaders(DittoHeaders, TopicPath, Extractor[])} is provided by inner class {@link Extractor}.
*/
public final class HeadersFromTopicPath {

private HeadersFromTopicPath() {
}

/**
* Injects new headers
*
* @param dittoHeaders the headers where the additional headers shall be injected into.
* @param topicPath where the headers will be extracted from.
* @param topicPathExtractors the extractor functions. Use {@link Extractor}
* @return new enriched ditto headers.
*/
public static DittoHeaders injectHeaders(final DittoHeaders dittoHeaders,
final TopicPath topicPath,
final Extractor... topicPathExtractors) {

final Map<String, String> headersFromTopicPath = Arrays.stream(topicPathExtractors)
.map(fn -> fn.apply(topicPath))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

return dittoHeaders
.toBuilder()
.putHeaders(headersFromTopicPath)
.build();
}

/**
* only a type declaration and a set of implemented extractor functions, which can be used for
* {@link #injectHeaders(DittoHeaders, TopicPath, Extractor[])}
*/
public interface Extractor extends Function<TopicPath, Optional<Map.Entry<String, String>>> {

/**
* Extracts the channel, if it is 'live'
*
* @param topicPath the topic path to extract information from.
* @return header KV containing the extra information from topic path.
*/
public static Optional<Map.Entry<String, String>> liveChannelExtractor(final TopicPath topicPath) {
if (topicPath.isChannel(TopicPath.Channel.LIVE)) {
final String key = DittoHeaderDefinition.CHANNEL.getKey();
final String value = TopicPath.Channel.LIVE.getName();

return Optional.of(new SimpleImmutableEntry<>(key, value));
} else {
return Optional.empty();
}
}

/**
* Extracts the entityId from the Topic-Path if none of the pieces is a placeholder ('_').
*
* @param topicPath the topic path to extract information from.
* @return header KV containing the extra information from topic path.
*/
static Optional<Map.Entry<String, String>> entityIdExtractor(final TopicPath topicPath) {
return getEntityId(topicPath)
.map(entityId -> new SimpleImmutableEntry<>(DittoHeaderDefinition.ENTITY_ID.getKey(), entityId));
}

}

private static Optional<String> getEntityId(final TopicPath topicPath) {
final TopicPath.Group group = topicPath.getGroup();
final String namespace = topicPath.getNamespace();
final String entityName = topicPath.getEntityName();
if (!TopicPath.ID_PLACEHOLDER.equals(namespace) && !TopicPath.ID_PLACEHOLDER.equals(entityName)) {
return Optional.of(String.join(":", group.getEntityType(), namespace, entityName));
} else {
return Optional.empty();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2021 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.protocol.adapter;

import static org.assertj.core.api.Assertions.assertThat;

import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.protocol.ProtocolFactory;
import org.eclipse.ditto.protocol.TopicPath;
import org.eclipse.ditto.things.model.ThingConstants;
import org.eclipse.ditto.things.model.ThingId;
import org.junit.Test;

/**
* Test for {@link HeadersFromTopicPath}
*/
public final class HeadersFromTopicPathTest {

@Test
public void shallExtractLiveChannel() {
// Arrange
final TopicPath topicPath = TopicPath.newBuilder(ThingId.of("org.eclipse.ditto:fancy-thing"))
.live()
.commands()
.modify()
.build();

// Act
final DittoHeaders dittoHeaders = HeadersFromTopicPath.injectHeaders(DittoHeaders.empty(),
topicPath,
HeadersFromTopicPath.Extractor::liveChannelExtractor);

// Assert
assertThat(dittoHeaders.getChannel()).hasValue("live");
}

@Test
public void shallNotExtractOtherChannels() {
// Arrange
final TopicPath topicPath = TopicPath.newBuilder(ThingId.of("org.eclipse.ditto:fancy-thing"))
.twin()
.commands()
.modify()
.build();

// Act
final DittoHeaders dittoHeaders = HeadersFromTopicPath.injectHeaders(DittoHeaders.empty(),
topicPath,
HeadersFromTopicPath.Extractor::liveChannelExtractor);

// Assert
assertThat(dittoHeaders).isEmpty();
}

@Test
public void shallNotFailWhenNoneChannel() {
// Arrange
final TopicPath topicPath = TopicPath.newBuilder(ThingId.of("org.eclipse.ditto:fancy-thing"))
.none()
.commands()
.modify()
.build();

// Act
final DittoHeaders dittoHeaders = HeadersFromTopicPath.injectHeaders(DittoHeaders.empty(),
topicPath,
HeadersFromTopicPath.Extractor::liveChannelExtractor);

// Assert
assertThat(dittoHeaders).isEmpty();
}

@Test
public void shallExtractEntityId() {
// Arrange
final ThingId thingId = ThingId.of("org.eclipse.ditto:fancy-thing");
final String expectedDittoEntityId = ThingConstants.ENTITY_TYPE + ":" + thingId;

final TopicPath topicPath = TopicPath.newBuilder(thingId)
.live()
.commands()
.modify()
.build();

// Act
final DittoHeaders dittoHeaders = HeadersFromTopicPath.injectHeaders(DittoHeaders.empty(),
topicPath,
HeadersFromTopicPath.Extractor::entityIdExtractor);

// Assert
assertThat(dittoHeaders.get(DittoHeaderDefinition.ENTITY_ID.getKey())).isEqualTo(expectedDittoEntityId);
}

@Test
public void shallNotFailWithPlaceholder() {
// Arrange
final TopicPath topicPath = ProtocolFactory.newTopicPath("_/_/things/twin/commands/modify");

// Act
final DittoHeaders dittoHeaders = HeadersFromTopicPath.injectHeaders(DittoHeaders.empty(),
topicPath,
HeadersFromTopicPath.Extractor::entityIdExtractor);

// Assert
assertThat(dittoHeaders).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package org.eclipse.ditto.protocol.adapter.things;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.base.model.common.HttpStatus;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
Expand Down Expand Up @@ -67,6 +69,27 @@ public void testFromAdaptable() {
assertWithExternalHeadersThat(actual).isEqualTo(expected);
}

@Test
public void testFromAdaptableWithChannelLive() {
final ThingErrorResponse expected =
ThingErrorResponse.of(TestConstants.THING_ID, dittoRuntimeException);

final TopicPath topicPath =
TopicPath.newBuilder(TestConstants.THING_ID).things().live().errors().build();
final JsonPointer path = JsonPointer.empty();

final Adaptable adaptable = Adaptable.newBuilder(topicPath)
.withPayload(Payload.newBuilder(path)
.withValue(dittoRuntimeException.toJson(FieldType.regularOrSpecial()))
.build())
.withHeaders(TestConstants.HEADERS_V_2)
.build();
final ThingErrorResponse actual = underTest.fromAdaptable(adaptable);

assertWithExternalHeadersThat(actual).isEqualTo(expected);
assertThat(actual.getDittoHeaders().getChannel()).hasValue("live");
}

@Test
public void testToAdaptable() {
final ThingErrorResponse errorResponse =
Expand Down

0 comments on commit 7cdff17

Please sign in to comment.