Skip to content

Commit

Permalink
Merge pull request #797 from bosch-io/feature/implicit-thing-creation
Browse files Browse the repository at this point in the history
Issue #760: Implicit creation of things
  • Loading branch information
jokraehe committed Sep 10, 2020
2 parents f14313a + d513f4e commit 35631f3
Show file tree
Hide file tree
Showing 29 changed files with 1,212 additions and 99 deletions.
Expand Up @@ -65,6 +65,7 @@ The following message mappers are included in the Ditto codebase:
| [Normalized](#normalized-mapper) | Transforms the payload of events to a normalized view. | ||
| [ConnectionStatus](#connectionstatus-mapper) | This mapper handles messages containing `creation-time` and `ttd` headers by updating a feature of the targeted thing with [definition](basic-feature.html#feature-definition) [ConnectionStatus](https://vorto.eclipse.org/#/details/org.eclipse.ditto:ConnectionStatus:1.0.0). || |
| [RawMessage](#rawmessage-mapper) | For outgoing message commands and responses, this mapper extracts the payload for publishing directly into the channel. For incoming messages, this mapper wraps them in a configured message command or response envelope. |||
| [ImplicitThingCreation](#implicitthingcreation-mapper) | This mapper handles messages for which a Thing should be created automatically based on a defined template|| |

### Ditto mapper

Expand Down Expand Up @@ -244,6 +245,29 @@ Example configuration:
* `ditto-message-feature-id` (optional): Include to send the message or message response to a feature of the thing.
Exclude to send it to the thing itself. Default to `{%raw%}{{ header:ditto-message-feature-id }}{%endraw%}`.

### ImplicitThingCreation Mapper
This mapper implicitly creates a new thing for an incoming message.

The created thing contains the values defined in the template, configured in the `mappingDefinitions` `options`.<br/>

#### Configuration options

* `thing` (required): The values of the thing that is created implicitly. It can either contain fixed values
or header placeholders (e.g. `{%raw%}{{ header:device_id }}{%endraw%}`).

Example of a template defined in `options`:
```json
{
"thing": {
"thingId": "{{ header:device_id }}",
"attributes": {
"CreatedBy": "ImplicitThingCreation"
}
}
}
```


## Example connection with multiple mappers

The following example connection defines a `ConnectionStatus` mapping with the ID `status` and references it in a
Expand All @@ -253,7 +277,6 @@ The following example connection defines a `ConnectionStatus` mapping with the I

```json
{
...
"name": "exampleConnection",
"sources": [{
"addresses": ["<source>"],
Expand All @@ -277,6 +300,42 @@ The following example connection defines a `ConnectionStatus` mapping with the I



## Example connection with mapping conditions

The following example connection defines `incomingConditions` and `outgoingConditions`for the ConnectionStatus mapping engine.<br/>
Optional incomingConditions are validated before the mapping of inbound messages.<br/>
Optional outgoingConditions are validated before the mapping of outbound messages.<br/>
Conditional Mapping can be achieved by using [function expressions](basic-placeholder.html#function-expressions).
When multiple incoming or outgoing conditions are set for one `mappingEngine`, all have to equal true for the mapping to be executed.

```json
{
"name": "exampleConnection",
"sources": [{
"addresses": ["<source>"],
"authorizationContext": ["ditto:inbound"],
"payloadMapping": ["status"]
}
],
"mappingDefinitions": {
"status": {
"mappingEngine": "ConnectionStatus",
"incomingConditions": {
"sampleCondition": "fn:filter(header:incoming-mapping-required,'eq','true')"
},
"outgoingConditions": {
"sampleCondition": "fn:filter(header:outgoing-mapping-required,'eq','true')"
},
"options": {
"thingId": "{%raw%}{{ header:device_id }}{%endraw%}"
}
}
}
}
```



## JavaScript mapping engine

Ditto utilizes the [Rhino](https://github.com/mozilla/rhino) JavaScript engine for Java for evaluating the JavaScript
Expand Down
Expand Up @@ -376,16 +376,31 @@ public static AddressMetric addressMetricFromJson(final JsonObject jsonObject) {
/**
* Returns a new {@code MappingContext}.
*
* @param mappingEngine fully qualified classname of a mapping engine
* @param options the mapping options required to instantiate a mapper
* @param mappingEngine fully qualified classname of a mapping engine.
* @param options the mapping options required to instantiate a mapper.
* @return the created MappingContext.
* @throws NullPointerException if any argument is {@code null}.
*
* @since 1.3.0
*/
public static MappingContextBuilder newMappingContextBuilder(final String mappingEngine,
final JsonObject options) {
return new ImmutableMappingContext.Builder(mappingEngine, options);
}

/**
* Returns a new {@code MappingContext}.
*
* @param mappingEngine fully qualified classname of a mapping engine.
* @param options the mapping options required to instantiate a mapper.
* @return the created MappingContext.
* @throws NullPointerException if any argument is {@code null}.
*/
public static MappingContext newMappingContext(final String mappingEngine, final Map<String, String> options) {
return newMappingContext(mappingEngine, options.entrySet()
return newMappingContextBuilder(mappingEngine, options.entrySet()
.stream()
.map(entry -> JsonField.newInstance(entry.getKey(), JsonValue.of(entry.getValue())))
.collect(JsonCollectors.fieldsToObject()));
.collect(JsonCollectors.fieldsToObject())).build();
}

/**
Expand All @@ -401,6 +416,24 @@ public static MappingContext newMappingContext(final String mappingEngine, final
return ImmutableMappingContext.of(mappingEngine, options);
}

/**
* Returns a new {@code MappingContext}.
*
* @param mappingEngine fully qualified classname of a mapping engine.
* @param options the mapping options required to instantiate a mapper.
* @param incomingConditions the conditions to be checked before mapping incoming messages.
* @param outgoingConditions the conditions to be checked before mapping outgoing messages.
* @return the created MappingContext.
* @throws NullPointerException if any argument is {@code null}.
* @since 1.3.0
*/
public static MappingContext newMappingContext(final String mappingEngine, final JsonObject options,
final Map<String, String> incomingConditions, final Map<String, String> outgoingConditions) {
return newMappingContextBuilder(mappingEngine, options).incomingConditions(incomingConditions)
.outgoingConditions(outgoingConditions)
.build();
}

/**
* Creates a new {@code MappingContext} object from the specified JSON object.
*
Expand Down
Expand Up @@ -14,15 +14,29 @@

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

import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;

import org.eclipse.ditto.json.JsonCollectors;
import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonField;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonObjectBuilder;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.model.base.acks.AcknowledgementLabel;
import org.eclipse.ditto.model.base.auth.AuthorizationContext;
import org.eclipse.ditto.model.base.json.JsonSchemaVersion;

/**
Expand All @@ -33,12 +47,16 @@ final class ImmutableMappingContext implements MappingContext {

private final String mappingEngine;
private final JsonObject options;


private ImmutableMappingContext(final String mappingEngine, final JsonObject options) {

this.mappingEngine = mappingEngine;
this.options = options;
private final Map<String, String> incomingConditions;
private final Map<String, String> outgoingConditions;

private ImmutableMappingContext(final ImmutableMappingContext.Builder builder) {
this.mappingEngine = builder.mappingEngine;
this.options = builder.options;
this.incomingConditions = Collections.unmodifiableMap(new HashMap<>(
builder.incomingConditions == null ? Collections.emptyMap() : builder.incomingConditions));
this.outgoingConditions = Collections.unmodifiableMap(new HashMap<>(
builder.outgoingConditions == null ? Collections.emptyMap() : builder.outgoingConditions));
}

/**
Expand All @@ -48,12 +66,13 @@ private ImmutableMappingContext(final String mappingEngine, final JsonObject opt
* {@code MessageMapper} interface.
* @param options the mapping engine specific options to apply.
* @return a new instance of ImmutableMappingContext.
* @deprecated Use {@link org.eclipse.ditto.model.connectivity.ImmutableMappingContext.Builder} instead.
*/
static ImmutableMappingContext of(final String mappingEngine, final JsonObject options) {
checkNotNull(mappingEngine, "mapping Engine");
checkNotNull(mappingEngine, "mappingEngine");
checkNotNull(options, "options");

return new ImmutableMappingContext(mappingEngine, options);
return (ImmutableMappingContext) new Builder(mappingEngine, options).build();
}

/**
Expand All @@ -65,10 +84,27 @@ static ImmutableMappingContext of(final String mappingEngine, final JsonObject o
* @throws org.eclipse.ditto.json.JsonParseException if {@code jsonObject} is not an appropriate JSON object.
*/
public static MappingContext fromJson(final JsonObject jsonObject) {

final String mappingEngine = jsonObject.getValueOrThrow(JsonFields.MAPPING_ENGINE);
final JsonObject options = jsonObject.getValueOrThrow(JsonFields.OPTIONS);

return of(mappingEngine, options);
final ImmutableMappingContext.Builder builder = new Builder(mappingEngine, options);

builder.incomingConditions(
jsonObject.getValue(JsonFields.INCOMING_CONDITIONS).orElse(JsonObject.empty()).stream()
.collect(Collectors.toMap(
e -> e.getKey().toString(),
e -> e.getValue().isString() ? e.getValue().asString() : e.getValue().toString())
));

builder.outgoingConditions(
jsonObject.getValue(JsonFields.OUTGOING_CONDITIONS).orElse(JsonObject.empty()).stream()
.collect(Collectors.toMap(
e -> e.getKey().toString(),
e -> e.getValue().isString() ? e.getValue().asString() : e.getValue().toString())
));

return builder.build();
}

@Override
Expand All @@ -77,8 +113,21 @@ public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate<
final JsonObjectBuilder jsonObjectBuilder = JsonFactory.newObjectBuilder();

jsonObjectBuilder.set(JsonFields.MAPPING_ENGINE, mappingEngine, predicate);

jsonObjectBuilder.set(JsonFields.OPTIONS, options, predicate);

if (!incomingConditions.isEmpty()) {
jsonObjectBuilder.set(JsonFields.INCOMING_CONDITIONS, incomingConditions.entrySet().stream()
.map(e -> JsonField.newInstance(e.getKey(), JsonValue.of(e.getValue())))
.collect(JsonCollectors.fieldsToObject()), predicate);
}

if (!outgoingConditions.isEmpty()) {
jsonObjectBuilder.set(JsonFields.OUTGOING_CONDITIONS, outgoingConditions.entrySet().stream()
.map(e -> JsonField.newInstance(e.getKey(), JsonValue.of(e.getValue())))
.collect(JsonCollectors.fieldsToObject()), predicate);
}

return jsonObjectBuilder.build();
}

Expand All @@ -92,6 +141,16 @@ public JsonObject getOptionsAsJson() {
return options;
}

@Override
public Map<String, String> getIncomingConditions() {
return incomingConditions;
}

@Override
public Map<String, String> getOutgoingConditions() {
return outgoingConditions;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
Expand All @@ -102,19 +161,73 @@ public boolean equals(final Object o) {
}
final ImmutableMappingContext that = (ImmutableMappingContext) o;
return Objects.equals(mappingEngine, that.mappingEngine) &&
Objects.equals(options, that.options);
Objects.equals(options, that.options) &&
Objects.equals(incomingConditions, that.incomingConditions) &&
Objects.equals(outgoingConditions, that.outgoingConditions);
}

@Override
public int hashCode() {
return Objects.hash(mappingEngine, options);
return Objects.hash(mappingEngine, options, incomingConditions, outgoingConditions);
}

@Override
public String toString() {
return getClass().getSimpleName() + " [" +
"mappingEngine=" + mappingEngine +
", options=" + options +
", incomingConditions=" + incomingConditions +
", outgoingConditions=" + outgoingConditions +
"]";
}

/**
* Builder for {@code ImmutableMappingContext}.
*
* @since 1.3.0
*/
@NotThreadSafe
static final class Builder implements MappingContextBuilder {

private String mappingEngine;
private JsonObject options;
@Nullable private Map<String, String> incomingConditions;
@Nullable private Map<String, String> outgoingConditions;

Builder(String mappingEngine, JsonObject options) {
this.mappingEngine = mappingEngine;
this.options = options;
}

@Override
public MappingContextBuilder mappingEngine(final String mappingEngine) {
this.mappingEngine = mappingEngine;
return this;
}

@Override
public MappingContextBuilder options(final JsonObject options) {
this.options = options;
return this;
}

@Override
public MappingContextBuilder incomingConditions(final Map<String, String> incomingConditions) {
this.incomingConditions = incomingConditions;
return this;
}

@Override
public MappingContextBuilder outgoingConditions(final Map<String, String> outgoingConditions) {
this.outgoingConditions = outgoingConditions;
return this;
}

@Override
public MappingContext build() {
checkNotNull(mappingEngine, "mappingEngine");
checkNotNull(options, "options");
return new ImmutableMappingContext(this);
}
}
}

0 comments on commit 35631f3

Please sign in to comment.