Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
Expand All @@ -29,6 +30,9 @@
import org.eclipse.ditto.client.options.Option;
import org.eclipse.ditto.client.options.OptionName;
import org.eclipse.ditto.client.options.internal.OptionsEvaluator;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonObjectBuilder;
import org.eclipse.ditto.json.JsonPointer;

/**
* This class provides the means to build {@link DittoHeaders} for a particular outgoing message.
Expand Down Expand Up @@ -97,6 +101,7 @@ private DittoHeaders getDittoHeaders() {
setEntityTagMatchers();
setCondition();
setLiveChannelCondition();
setMergeThingPatchConditions();
return buildDittoHeaders();
}

Expand Down Expand Up @@ -166,6 +171,18 @@ private void setLiveChannelCondition() {
});
}

private void setMergeThingPatchConditions() {
globalOptionsEvaluator.getMergeThingPatchConditions()
.ifPresent(patchConditions -> {
validateIfOptionIsAllowed(OptionName.Global.MERGE_THING_PATCH_CONDITIONS);
final JsonObjectBuilder builder = JsonObject.newBuilder();
for (final Map.Entry<JsonPointer, String> entry : patchConditions.entrySet()) {
builder.set(entry.getKey().toString(), entry.getValue());
}
headersBuilder.putHeader("merge-thing-patch-conditions", builder.build().toString());
});
}

private DittoHeaders buildDittoHeaders() {
return headersBuilder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;
import static org.eclipse.ditto.client.options.OptionName.Global.CONDITION;
import static org.eclipse.ditto.client.options.OptionName.Global.LIVE_CHANNEL_CONDITION;
import static org.eclipse.ditto.client.options.OptionName.Global.MERGE_THING_PATCH_CONDITIONS;
import static org.eclipse.ditto.client.options.OptionName.Modify.EXISTS;

import java.nio.charset.Charset;
Expand Down Expand Up @@ -341,7 +342,7 @@ public MergeThing mergeAttribute(final ThingId thingId,
return MergeThing.withAttribute(thingId,
path,
value,
buildDittoHeaders(setOf(EXISTS, CONDITION), options));
buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

public ModifyAttributes setAttributes(final ThingId thingId,
Expand All @@ -356,7 +357,7 @@ public ModifyAttributes setAttributes(final ThingId thingId,
public MergeThing mergeAttributes(final ThingId thingId, final JsonObject attributes, final Option<?>[] options) {
return MergeThing.withAttributes(thingId,
ThingsModelFactory.newAttributes(attributes),
buildDittoHeaders(setOf(EXISTS, CONDITION), options));
buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

public DeleteAttribute deleteAttribute(final ThingId thingId, final JsonPointer path, final Option<?>... options) {
Expand All @@ -372,23 +373,23 @@ public ModifyFeature setFeature(final ThingId thingId, final Feature feature, fi
}

public MergeThing mergeFeature(final ThingId thingId, final Feature feature, final Option<?>... options) {
return MergeThing.withFeature(thingId, feature, buildDittoHeaders(setOf(EXISTS, CONDITION), options));
return MergeThing.withFeature(thingId, feature, buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

public ModifyFeatures setFeatures(final ThingId thingId, final Features features, final Option<?>... options) {
return ModifyFeatures.of(thingId, features, buildDittoHeaders(setOf(EXISTS, CONDITION), options));
}

public MergeThing mergeFeatures(final ThingId thingId, final Features features, final Option<?>[] options) {
return MergeThing.withFeatures(thingId, features, buildDittoHeaders(setOf(EXISTS, CONDITION), options));
return MergeThing.withFeatures(thingId, features, buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

public ModifyPolicyId setPolicyId(final ThingId thingId, final PolicyId policyId, final Option<?>... options) {
return ModifyPolicyId.of(thingId, policyId, buildDittoHeaders(setOf(EXISTS, CONDITION), options));
}

public MergeThing mergePolicyId(final ThingId thingId, final PolicyId policyId, final Option<?>... options) {
return MergeThing.withPolicyId(thingId, policyId, buildDittoHeaders(setOf(EXISTS, CONDITION), options));
return MergeThing.withPolicyId(thingId, policyId, buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

public RetrieveFeature retrieveFeature(final ThingId thingId, final String featureId, final Option<?>... options) {
Expand Down Expand Up @@ -455,7 +456,7 @@ public MergeThing mergeFeatureDefinition(final ThingId thingId,
return MergeThing.withFeatureDefinition(thingId,
featureId,
featureDefinition,
buildDittoHeaders(setOf(EXISTS, CONDITION), options));
buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

/**
Expand Down Expand Up @@ -497,7 +498,7 @@ public MergeThing mergeFeatureProperty(final ThingId thingId,
featureId,
path,
value,
buildDittoHeaders(setOf(EXISTS, CONDITION), options));
buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

public ModifyFeatureProperties setFeatureProperties(final ThingId thingId,
Expand All @@ -519,7 +520,7 @@ public MergeThing mergeFeatureProperties(final ThingId thingId,
return MergeThing.withFeatureProperties(thingId,
featureId,
ThingsModelFactory.newFeatureProperties(properties),
buildDittoHeaders(setOf(EXISTS, CONDITION), options));
buildDittoHeaders(setOf(EXISTS, CONDITION, MERGE_THING_PATCH_CONDITIONS), options));
}

public DeleteFeatureProperty deleteFeatureProperty(final ThingId thingId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ enum Global implements OptionName {
*/
LIVE_CHANNEL_CONDITION,

/**
* Name of the option for defining merge thing patch conditions, which map JSON pointer paths to RQL condition expressions.
* These conditions determine which parts of a merge payload should be applied based on the current state of the Thing.
*
* @since 3.8.0
*/
MERGE_THING_PATCH_CONDITIONS,

}

/**
Expand Down
27 changes: 27 additions & 0 deletions java/src/main/java/org/eclipse/ditto/client/options/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
package org.eclipse.ditto.client.options;

import java.util.Arrays;
import java.util.Map;

import org.eclipse.ditto.base.model.common.ConditionChecker;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.client.management.CommonManagement;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.policies.model.PolicyId;
import org.eclipse.ditto.things.model.ThingId;

Expand Down Expand Up @@ -90,6 +92,31 @@ public static Option<String> liveChannelCondition(final CharSequence liveChannel
liveChannelConditionExpression.toString());
}

/**
* Creates an option for specifying merge thing patch conditions.
* <p>
* The returned option has the name {@link OptionName.Global#MERGE_THING_PATCH_CONDITIONS} and the given argument value.
* </p>
* <p>
* The patch conditions map JSON pointer paths to RQL condition expressions. These conditions determine which parts
* of a merge payload should be applied based on the current state of the Thing.
* </p>
*
* @param patchConditions the map of JSON pointer paths to RQL condition expressions
* @return the new {@code Option}.
* @throws NullPointerException if {@code patchConditions} is {@code null}.
* @throws IllegalArgumentException if {@code patchConditions} is empty.
* @since 3.8.0
*/
public static Option<Map<JsonPointer, String>> mergeThingPatchConditions(final Map<JsonPointer, String> patchConditions) {
ConditionChecker.checkNotNull(patchConditions, "patchConditions");
ConditionChecker.checkArgument(patchConditions,
argument -> !argument.isEmpty(),
() -> "The patchConditions map must not be empty.");

return DefaultOption.newInstance(OptionName.Global.MERGE_THING_PATCH_CONDITIONS, patchConditions);
}

/**
* The {@code Modify} class provides static factory methods for creating Options which are related to modifying
* operations.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 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.client.options.internal;

import java.util.Map;

import javax.annotation.concurrent.ThreadSafe;

import org.eclipse.ditto.client.options.Option;
import org.eclipse.ditto.client.options.OptionName;
import org.eclipse.ditto.json.JsonPointer;


/**
* This visitor fetches and provides the value as {@code Map<JsonPointer, String>} for the option with name
* {@link OptionName.Global#MERGE_THING_PATCH_CONDITIONS} from the user provided options.
*
* @since 3.8.0
*/
@ThreadSafe
final class MergeThingPatchConditionsOptionVisitor extends AbstractOptionVisitor<Map<JsonPointer, String>> {

/**
* Constructs a {@code MergeThingPatchConditionsOptionVisitor} object.
*/
MergeThingPatchConditionsOptionVisitor() {
super(OptionName.Global.MERGE_THING_PATCH_CONDITIONS);
}

@Override
@SuppressWarnings("unchecked")
protected Map<JsonPointer, String> getValueFromOption(final Option<?> option) {
return option.getValueAs(Map.class);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
*/
package org.eclipse.ditto.client.options.internal;

import java.util.Map;
import java.util.Optional;

import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.client.options.Option;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.policies.model.PolicyId;
import org.eclipse.ditto.things.model.ThingId;

Expand Down Expand Up @@ -132,6 +134,17 @@ public Optional<String> getLiveChannelCondition() {
return getValue(new LiveChannelConditionOptionVisitor());
}

/**
* Returns the merge thing patch conditions as provided by the user.
*
* @return an Optional containing the map of JSON pointer paths to RQL condition expressions
* if provided by the user, an empty Optional else.
* @since 3.8.0
*/
public Optional<Map<JsonPointer, String>> getMergeThingPatchConditions() {
return getValue(new MergeThingPatchConditionsOptionVisitor());
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
*/
package org.eclipse.ditto.client.options;

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

import org.assertj.core.api.Assertions;
import org.assertj.core.api.JUnitSoftAssertions;
import org.eclipse.ditto.json.JsonPointer;
import org.junit.Rule;
import org.junit.Test;

Expand Down Expand Up @@ -59,4 +63,34 @@ public void lifeChannelConditionWithValidExpressionReturnsExpected() {
softly.assertThat(option.getValue()).as("option value").isEqualTo(liveChannelConditionExpression);
}

@Test
public void mergeThingPatchConditionsWithNullMapThrowsException() {
Assertions.assertThatNullPointerException()
.isThrownBy(() -> Options.mergeThingPatchConditions(null))
.withMessage("The patchConditions must not be null!")
.withNoCause();
}

@Test
public void mergeThingPatchConditionsWithEmptyMapThrowsException() {
final Map<JsonPointer, String> emptyMap = new HashMap<>();

Assertions.assertThatIllegalArgumentException()
.isThrownBy(() -> Options.mergeThingPatchConditions(emptyMap))
.withMessage("The patchConditions map must not be empty.")
.withNoCause();
}

@Test
public void mergeThingPatchConditionsWithValidMapReturnsExpected() {
final Map<JsonPointer, String> patchConditions = new HashMap<>();
patchConditions.put(JsonPointer.of("features/temperature/properties/value"), "gt(features/temperature/properties/value, 20)");
patchConditions.put(JsonPointer.of("features/humidity/properties/value"), "lt(features/humidity/properties/value, 80)");

final Option<Map<JsonPointer, String>> option = Options.mergeThingPatchConditions(patchConditions);

softly.assertThat(option.getName()).as("option name").isEqualTo(OptionName.Global.MERGE_THING_PATCH_CONDITIONS);
softly.assertThat(option.getValue()).as("option value").isEqualTo(patchConditions);
}

}
Loading