Skip to content

Commit

Permalink
Issue #1228: change header values for live channel timeout fallback f…
Browse files Browse the repository at this point in the history
…rom duration to a strategy enum.

Signed-off-by: Yufei Cai <yufei.cai@bosch.io>
  • Loading branch information
yufei-cai committed Nov 29, 2021
1 parent acdfc76 commit bbcc567
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,9 @@ public Optional<Duration> getTimeout() {
}

@Override
public Optional<Duration> getTwinFallbackAfter() {
return getStringForDefinition(DittoHeaderDefinition.TWIN_FALLBACK_AFTER)
.map(DittoDuration::parseDuration)
.map(DittoDuration::getDuration);
public Optional<LiveChannelTimeoutStrategy> getLiveChannelTimeoutStrategy() {
return getStringForDefinition(DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT)
.flatMap(LiveChannelTimeoutStrategy::forHeaderValue);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
* <p>
* Key: {@code "channel"}, Java type: {@link String}.
* </p>
*
* @since 2.3.0
*/
CHANNEL("channel", String.class, true, true,
Expand Down Expand Up @@ -162,7 +163,7 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
* Key: {@code "If-None-Match"}, Java type: {@link String}.
* </p>
*/
IF_NONE_MATCH("if-none-match", EntityTagMatchers.class, String.class,true,
IF_NONE_MATCH("if-none-match", EntityTagMatchers.class, String.class, true,
false, HeaderValueValidators.getEntityTagMatchersValidator()),

/**
Expand Down Expand Up @@ -245,10 +246,10 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
* Key: {@code "twin-fallback-after"}, Java type: {@code String}.
* </p>
*
* @since 2.2.0
* @since 2.3.0
*/
TWIN_FALLBACK_AFTER("twin-fallback-after", DittoDuration.class, String.class, true, false,
HeaderValueValidators.getTimeoutValueValidator()),
ON_LIVE_CHANNEL_TIMEOUT("on-live-channel-timeout", LiveChannelTimeoutStrategy.class, String.class, true, false,
HeaderValueValidators.getEnumValidator(LiveChannelTimeoutStrategy.values())),

/**
* Header definition for the entity ID related to the command/event/response/error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,13 @@ static DittoHeadersBuilder newBuilder(final JsonObject jsonObject) {
Optional<Duration> getTimeout();

/**
* Returns when a thing query command with smart channel selection should wait for a live response
* before falling back to the twin response.
* Returns what to do when a thing query command with smart channel selection does not receive a valid live
* response within the given timeout period.
*
* @return Time allocated to the live response in a thing query command with smart channel selection.
* @since 2.2.0
* @return the live channel timeout strategy if the headers define any.
* @since 2.3.0
*/
Optional<Duration> getTwinFallbackAfter();
Optional<LiveChannelTimeoutStrategy> getLiveChannelTimeoutStrategy();

/**
* Returns the metadata headers to put/set for the (modifying) command they were added to.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.base.model.headers;

import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.base.model.exceptions.DittoHeaderInvalidException;

/**
* This validator checks if a normalized CharSequence denote an enum value.
*
* @since 2.3.0
*/
@Immutable
final class EnumValueValidator extends AbstractHeaderValueValidator {

private final Set<String> enumValueSet;
private final String errorDescription;

private EnumValueValidator(final Enum<?>[] enumValues) {
super(String.class::equals);
enumValueSet = groupByNormalizedName(enumValues);
errorDescription = formatErrorDescription(enumValueSet);
}

/**
* Returns an instance of {@code DittoChannelValueValidator}.
*
* @return the instance.
*/
static EnumValueValidator getInstance(final Enum<?>[] enumValues) {
return new EnumValueValidator(enumValues);
}

@Override
protected void validateValue(final HeaderDefinition definition, final CharSequence value) {
final String normalizedValue = normalize(value);
if (!enumValueSet.contains(normalizedValue)) {
throw DittoHeaderInvalidException.newInvalidTypeBuilder(definition, value,
DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT.getKey())
.description(errorDescription)
.build();
}
}

private static String normalize(final CharSequence charSequence) {
return charSequence.toString().trim().toLowerCase(Locale.ENGLISH);
}

private static Set<String> groupByNormalizedName(final Enum<?>[] enumValues) {
final Set<String> set = Arrays.stream(enumValues)
.map(Enum::toString)
.map(EnumValueValidator::normalize)
.collect(Collectors.toSet());
return Collections.unmodifiableSet(set);
}

private static String formatErrorDescription(final Collection<String> normalizedNames) {
final String valuesString = normalizedNames.stream()
.map(name -> "<" + name + ">")
.collect(Collectors.joining(", "));
return MessageFormat.format("The value must either be one of: {0}.", valuesString);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ public static ValueValidator getBooleanValidator() {
return BooleanValueValidator.getInstance();
}

/**
* Returns a validator checking a header value against the string representation of members of an enum class.
*
* @param enumValues The values of the enum class.
* @return The validator.
* @since 2.3.0
*/
public static ValueValidator getEnumValidator(final Enum<?>[] enumValues) {
return EnumValueValidator.getInstance(enumValues);
}

/**
* Returns a validator for checking if a CharSequence represents a {@link org.eclipse.ditto.json.JsonArray} which
* <em>contains only string items.</em>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.base.model.headers;

import java.util.Arrays;
import java.util.Optional;

/**
* Headers for commands and their responses which provide additional information needed for correlation and transfer.
*
* @since 2.3.0
*/
public enum LiveChannelTimeoutStrategy {

FAIL("fail"),

USE_TWIN("use-twin");

private final String headerValue;

LiveChannelTimeoutStrategy(final String headerValue) {
this.headerValue = headerValue;
}

@Override
public String toString() {
return headerValue;
}

/**
* Find a live channel timeout strategy by header value.
*
* @param headerValue the header value of the strategy.
* @return the strategy with the given header value if any exists.
*/
public static Optional<LiveChannelTimeoutStrategy> forHeaderValue(final String headerValue) {
return Arrays.stream(values())
.filter(strategy -> strategy.toString().equals(headerValue))
.findAny();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public void settingAllKnownHeadersWorksAsExpected() {
.acknowledgementRequests(KNOWN_ACK_REQUESTS)
.putMetadata(KNOWN_METADATA_HEADER_KEY, KNOWN_METADATA_VALUE)
.timeout(KNOWN_TIMEOUT)
.putHeader(DittoHeaderDefinition.TWIN_FALLBACK_AFTER.getKey(), KNOWN_TIMEOUT.toMillis() + "ms")
.putHeader(DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT.getKey(), "use-twin")
.putHeader(DittoHeaderDefinition.CONNECTION_ID.getKey(), KNOWN_CONNECTION_ID)
.expectedResponseTypes(KNOWN_EXPECTED_RESPONSE_TYPES)
.allowPolicyLockout(KNOWN_ALLOW_POLICY_LOCKOUT)
Expand Down Expand Up @@ -353,6 +353,21 @@ public void getRequestedAckLabelsReturnsExpected() {
assertThat(underTest.getAcknowledgementRequests()).containsExactlyInAnyOrderElementsOf(KNOWN_ACK_REQUESTS);
}

@Test
public void getLiveChannelTimeoutStrategy() {
assertThat(DittoHeaders.newBuilder()
.putHeader(DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT.getKey(), "use-twin")
.build()
.getLiveChannelTimeoutStrategy())
.contains(LiveChannelTimeoutStrategy.USE_TWIN);

assertThat(DittoHeaders.newBuilder()
.putHeader(DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT.getKey(), "fail")
.build()
.getLiveChannelTimeoutStrategy())
.contains(LiveChannelTimeoutStrategy.FAIL);
}

@Test
public void timeoutIsSerializedAsString() {
final int durationAmountSeconds = 2;
Expand Down Expand Up @@ -417,7 +432,7 @@ public void toJsonReturnsExpected() {
.set(DittoHeaderDefinition.REQUESTED_ACKS.getKey(), ackRequestsToJsonArray(KNOWN_ACK_REQUESTS))
.set(DittoHeaderDefinition.DECLARED_ACKS.getKey(), charSequencesToJsonArray(KNOWN_ACK_LABELS))
.set(DittoHeaderDefinition.TIMEOUT.getKey(), JsonValue.of(KNOWN_TIMEOUT.toMillis() + "ms"))
.set(DittoHeaderDefinition.TWIN_FALLBACK_AFTER.getKey(), JsonValue.of(KNOWN_TIMEOUT.toMillis() + "ms"))
.set(DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT.getKey(), JsonValue.of("use-twin"))
.set(DittoHeaderDefinition.ENTITY_ID.getKey(), KNOWN_ENTITY_ID)
.set(DittoHeaderDefinition.REPLY_TO.getKey(), KNOWN_REPLY_TO)
.set(DittoHeaderDefinition.WWW_AUTHENTICATE.getKey(), KNOWN_WWW_AUTHENTICATION)
Expand Down Expand Up @@ -648,7 +663,7 @@ private static Map<String, String> createMapContainingAllKnownHeaders() {
result.put(DittoHeaderDefinition.DECLARED_ACKS.getKey(),
charSequencesToJsonArray(KNOWN_ACK_LABELS).toString());
result.put(DittoHeaderDefinition.TIMEOUT.getKey(), KNOWN_TIMEOUT.toMillis() + "ms");
result.put(DittoHeaderDefinition.TWIN_FALLBACK_AFTER.getKey(), KNOWN_TIMEOUT.toMillis() + "ms");
result.put(DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT.getKey(), "use-twin");
result.put(DittoHeaderDefinition.ENTITY_ID.getKey(), KNOWN_ENTITY_ID);
result.put(DittoHeaderDefinition.REPLY_TO.getKey(), KNOWN_REPLY_TO);
result.put(DittoHeaderDefinition.WWW_AUTHENTICATE.getKey(), KNOWN_WWW_AUTHENTICATION);
Expand Down

0 comments on commit bbcc567

Please sign in to comment.