25 changed files with 856 additions and 187 deletions
@ -0,0 +1,59 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.TbNodeUtils; |
||||
|
import org.thingsboard.rule.engine.api.*; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import static org.thingsboard.rule.engine.DonAsynchron.withCallback; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.01.18. |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public abstract class TbAbstractTransformNode implements TbNode { |
||||
|
|
||||
|
private TbTransformNodeConfiguration config; |
||||
|
|
||||
|
@Override |
||||
|
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { |
||||
|
this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onMsg(TbContext ctx, TbMsg msg) { |
||||
|
withCallback(transform(ctx, msg), |
||||
|
m -> routeMsg(ctx, m), |
||||
|
t -> ctx.tellError(msg, t)); |
||||
|
} |
||||
|
|
||||
|
protected abstract ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg); |
||||
|
|
||||
|
private void routeMsg(TbContext ctx, TbMsg msg) { |
||||
|
if (config.isStartNewChain()) { |
||||
|
ctx.spawn(msg); |
||||
|
} else { |
||||
|
ctx.tellNext(msg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void setConfig(TbTransformNodeConfiguration config) { |
||||
|
this.config = config; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import com.google.common.base.Function; |
||||
|
import com.google.common.collect.Sets; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.thingsboard.rule.engine.TbNodeUtils; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeState; |
||||
|
import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; |
||||
|
import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; |
||||
|
import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import java.util.HashSet; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class TbChangeOriginatorNode extends TbAbstractTransformNode { |
||||
|
|
||||
|
protected static final String CUSTOMER_SOURCE = "CUSTOMER"; |
||||
|
protected static final String TENANT_SOURCE = "TENANT"; |
||||
|
protected static final String RELATED_SOURCE = "RELATED"; |
||||
|
|
||||
|
private TbChangeOriginatorNodeConfiguration config; |
||||
|
|
||||
|
@Override |
||||
|
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { |
||||
|
this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class); |
||||
|
validateConfig(config); |
||||
|
setConfig(config); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) { |
||||
|
ListenableFuture<? extends EntityId> newOriginator = getNewOriginator(ctx, msg.getOriginator()); |
||||
|
return Futures.transform(newOriginator, (Function<EntityId, TbMsg>) n -> new TbMsg(msg.getId(), msg.getType(), n, msg.getMetaData(), msg.getData())); |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<? extends EntityId> getNewOriginator(TbContext ctx, EntityId original) { |
||||
|
switch (config.getOriginatorSource()) { |
||||
|
case CUSTOMER_SOURCE: |
||||
|
return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, original); |
||||
|
case TENANT_SOURCE: |
||||
|
return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original); |
||||
|
case RELATED_SOURCE: |
||||
|
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getDirection(), config.getRelationType()); |
||||
|
default: |
||||
|
return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void validateConfig(TbChangeOriginatorNodeConfiguration conf) { |
||||
|
HashSet<String> knownSources = Sets.newHashSet(CUSTOMER_SOURCE, TENANT_SOURCE, RELATED_SOURCE); |
||||
|
if (!knownSources.contains(conf.getOriginatorSource())) { |
||||
|
log.error("Unsupported source [{}] for TbChangeOriginatorNode", conf.getOriginatorSource()); |
||||
|
throw new IllegalArgumentException("Unsupported source TbChangeOriginatorNode" + conf.getOriginatorSource()); |
||||
|
} |
||||
|
|
||||
|
if (conf.getOriginatorSource().equals(RELATED_SOURCE)) { |
||||
|
if (conf.getDirection() == null || StringUtils.isBlank(conf.getRelationType())) { |
||||
|
log.error("Related source for TbChangeOriginatorNode should have direction and relationType. Actual [{}] [{}]", |
||||
|
conf.getDirection(), conf.getRelationType()); |
||||
|
throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void destroy() { |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
||||
|
|
||||
|
@Data |
||||
|
public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration{ |
||||
|
|
||||
|
private String originatorSource; |
||||
|
private EntitySearchDirection direction; |
||||
|
private String relationType; |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.thingsboard.rule.engine.TbNodeUtils; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeState; |
||||
|
import org.thingsboard.rule.engine.js.NashornJsEngine; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import javax.script.Bindings; |
||||
|
|
||||
|
public class TbTransformMsgNode extends TbAbstractTransformNode { |
||||
|
|
||||
|
private TbTransformMsgNodeConfiguration config; |
||||
|
private NashornJsEngine jsEngine; |
||||
|
|
||||
|
@Override |
||||
|
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { |
||||
|
this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); |
||||
|
this.jsEngine = new NashornJsEngine(config.getJsScript()); |
||||
|
setConfig(config); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) { |
||||
|
return ctx.getJsExecutor().executeAsync(() -> jsEngine.executeUpdate(toBindings(msg), msg)); |
||||
|
} |
||||
|
|
||||
|
private Bindings toBindings(TbMsg msg) { |
||||
|
return NashornJsEngine.bindMsg(msg); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void destroy() { |
||||
|
if (jsEngine != null) { |
||||
|
jsEngine.destroy(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration { |
||||
|
|
||||
|
private String jsScript; |
||||
|
} |
||||
@ -1,68 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2018 The Thingsboard Authors |
|
||||
* |
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
* you may not use this file except in compliance with the License. |
|
||||
* You may obtain a copy of the License at |
|
||||
* |
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
* |
|
||||
* Unless required by applicable law or agreed to in writing, software |
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
* See the License for the specific language governing permissions and |
|
||||
* limitations under the License. |
|
||||
*/ |
|
||||
package org.thingsboard.rule.engine.transform; |
|
||||
|
|
||||
import lombok.extern.slf4j.Slf4j; |
|
||||
import org.thingsboard.rule.engine.TbNodeUtils; |
|
||||
import org.thingsboard.rule.engine.api.*; |
|
||||
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; |
|
||||
import org.thingsboard.server.common.data.DataConstants; |
|
||||
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
|
||||
import org.thingsboard.server.common.msg.TbMsg; |
|
||||
import org.thingsboard.server.dao.attributes.AttributesService; |
|
||||
|
|
||||
import java.util.List; |
|
||||
|
|
||||
/** |
|
||||
* Created by ashvayka on 19.01.18. |
|
||||
*/ |
|
||||
@Slf4j |
|
||||
public class TbTransformNode implements TbNode { |
|
||||
|
|
||||
TbGetAttributesNodeConfiguration config; |
|
||||
|
|
||||
@Override |
|
||||
public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException { |
|
||||
this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { |
|
||||
try { |
|
||||
//TODO: refactor this to work async and fetch attributes from cache.
|
|
||||
AttributesService service = ctx.getAttributesService(); |
|
||||
fetchAttributes(msg, service, config.getClientAttributeNames(), DataConstants.CLIENT_SCOPE, "cs."); |
|
||||
fetchAttributes(msg, service, config.getServerAttributeNames(), DataConstants.SERVER_SCOPE, "ss."); |
|
||||
fetchAttributes(msg, service, config.getSharedAttributeNames(), DataConstants.SHARED_SCOPE, "shared."); |
|
||||
ctx.tellNext(msg); |
|
||||
} catch (Exception e) { |
|
||||
log.warn("[{}][{}] Failed to fetch attributes", msg.getOriginator(), msg.getId(), e); |
|
||||
throw new TbNodeException(e); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void fetchAttributes(TbMsg msg, AttributesService service, List<String> attributeNames, String scope, String prefix) throws InterruptedException, java.util.concurrent.ExecutionException { |
|
||||
if (attributeNames != null && attributeNames.isEmpty()) { |
|
||||
List<AttributeKvEntry> attributes = service.find(msg.getOriginator(), scope, attributeNames).get(); |
|
||||
attributes.forEach(attr -> msg.getMetaData().putValue(prefix + attr.getKey(), attr.getValueAsString())); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void destroy() { |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class TbTransformNodeConfiguration { |
||||
|
|
||||
|
private boolean startNewChain = false; |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.util; |
||||
|
|
||||
|
import com.google.common.util.concurrent.AsyncFunction; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.HasCustomerId; |
||||
|
import org.thingsboard.server.common.data.id.*; |
||||
|
|
||||
|
public class EntitiesCustomerIdAsyncLoader { |
||||
|
|
||||
|
|
||||
|
public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId original) { |
||||
|
|
||||
|
switch (original.getEntityType()) { |
||||
|
case CUSTOMER: |
||||
|
return Futures.immediateFuture((CustomerId) original); |
||||
|
case USER: |
||||
|
return getCustomerAsync(ctx.getUserService().findUserByIdAsync((UserId) original)); |
||||
|
case ASSET: |
||||
|
return getCustomerAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original)); |
||||
|
case DEVICE: |
||||
|
return getCustomerAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original)); |
||||
|
default: |
||||
|
return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static <T extends HasCustomerId> ListenableFuture<CustomerId> getCustomerAsync(ListenableFuture<T> future) { |
||||
|
return Futures.transform(future, (AsyncFunction<HasCustomerId, CustomerId>) in -> { |
||||
|
return in != null ? Futures.immediateFuture(in.getCustomerId()) |
||||
|
: Futures.immediateFailedFuture(new IllegalStateException("Customer not found"));}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.util; |
||||
|
|
||||
|
import com.google.common.util.concurrent.AsyncFunction; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.apache.commons.collections.CollectionUtils; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.relation.EntityRelation; |
||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
||||
|
import org.thingsboard.server.dao.relation.RelationService; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
import static org.thingsboard.server.common.data.relation.RelationTypeGroup.COMMON; |
||||
|
|
||||
|
public class EntitiesRelatedEntityIdAsyncLoader { |
||||
|
|
||||
|
public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator, |
||||
|
EntitySearchDirection direction, String relationType) { |
||||
|
RelationService relationService = ctx.getRelationService(); |
||||
|
if (direction == EntitySearchDirection.FROM) { |
||||
|
ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByFromAndTypeAsync(originator, relationType, COMMON); |
||||
|
return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) |
||||
|
r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo()) |
||||
|
: Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); |
||||
|
} else if (direction == EntitySearchDirection.TO) { |
||||
|
ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByToAndTypeAsync(originator, relationType, COMMON); |
||||
|
return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>) |
||||
|
r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom()) |
||||
|
: Futures.immediateFailedFuture(new IllegalStateException("Relation not found"))); |
||||
|
} |
||||
|
|
||||
|
return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction")); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.util; |
||||
|
|
||||
|
import com.google.common.util.concurrent.AsyncFunction; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.HasTenantId; |
||||
|
import org.thingsboard.server.common.data.alarm.AlarmId; |
||||
|
import org.thingsboard.server.common.data.id.*; |
||||
|
|
||||
|
public class EntitiesTenantIdAsyncLoader { |
||||
|
|
||||
|
public static ListenableFuture<TenantId> findEntityIdAsync(TbContext ctx, EntityId original) { |
||||
|
|
||||
|
switch (original.getEntityType()) { |
||||
|
case TENANT: |
||||
|
return Futures.immediateFuture((TenantId) original); |
||||
|
case CUSTOMER: |
||||
|
return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync((CustomerId) original)); |
||||
|
case USER: |
||||
|
return getTenantAsync(ctx.getUserService().findUserByIdAsync((UserId) original)); |
||||
|
case RULE: |
||||
|
return getTenantAsync(ctx.getRuleService().findRuleByIdAsync((RuleId) original)); |
||||
|
case PLUGIN: |
||||
|
return getTenantAsync(ctx.getPluginService().findPluginByIdAsync((PluginId) original)); |
||||
|
case ASSET: |
||||
|
return getTenantAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original)); |
||||
|
case DEVICE: |
||||
|
return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original)); |
||||
|
case ALARM: |
||||
|
return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync((AlarmId) original)); |
||||
|
case RULE_CHAIN: |
||||
|
return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync((RuleChainId) original)); |
||||
|
default: |
||||
|
return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static <T extends HasTenantId> ListenableFuture<TenantId> getTenantAsync(ListenableFuture<T> future) { |
||||
|
return Futures.transform(future, (AsyncFunction<HasTenantId, TenantId>) in -> { |
||||
|
return in != null ? Futures.immediateFuture(in.getTenantId()) |
||||
|
: Futures.immediateFailedFuture(new IllegalStateException("Tenant not found"));}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import com.datastax.driver.core.utils.UUIDs; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.runners.MockitoJUnitRunner; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
|
||||
|
import static org.junit.Assert.assertEquals; |
||||
|
import static org.mockito.Matchers.same; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@RunWith(MockitoJUnitRunner.class) |
||||
|
public class TbChangeOriginatorNodeTest { |
||||
|
|
||||
|
private TbChangeOriginatorNode node; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctx; |
||||
|
@Mock |
||||
|
private AssetService assetService; |
||||
|
|
||||
|
|
||||
|
@Test |
||||
|
public void originatorCanBeChangedToCustomerId() throws TbNodeException { |
||||
|
init(false); |
||||
|
AssetId assetId = new AssetId(UUIDs.timeBased()); |
||||
|
CustomerId customerId = new CustomerId(UUIDs.timeBased()); |
||||
|
Asset asset = new Asset(); |
||||
|
asset.setCustomerId(customerId); |
||||
|
|
||||
|
TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), new byte[4]); |
||||
|
|
||||
|
when(ctx.getAssetService()).thenReturn(assetService); |
||||
|
when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset)); |
||||
|
|
||||
|
node.onMsg(ctx, msg); |
||||
|
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctx).tellNext(captor.capture()); |
||||
|
TbMsg actualMsg = captor.getValue(); |
||||
|
assertEquals(customerId, actualMsg.getOriginator()); |
||||
|
assertEquals(msg.getId(), actualMsg.getId()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void newChainCanBeStarted() throws TbNodeException { |
||||
|
init(true); |
||||
|
AssetId assetId = new AssetId(UUIDs.timeBased()); |
||||
|
CustomerId customerId = new CustomerId(UUIDs.timeBased()); |
||||
|
Asset asset = new Asset(); |
||||
|
asset.setCustomerId(customerId); |
||||
|
|
||||
|
TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), new byte[4]); |
||||
|
|
||||
|
when(ctx.getAssetService()).thenReturn(assetService); |
||||
|
when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset)); |
||||
|
|
||||
|
node.onMsg(ctx, msg); |
||||
|
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctx).spawn(captor.capture()); |
||||
|
TbMsg actualMsg = captor.getValue(); |
||||
|
assertEquals(customerId, actualMsg.getOriginator()); |
||||
|
assertEquals(msg.getId(), actualMsg.getId()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void exceptionThrownIfCannotFindNewOriginator() throws TbNodeException { |
||||
|
init(true); |
||||
|
AssetId assetId = new AssetId(UUIDs.timeBased()); |
||||
|
CustomerId customerId = new CustomerId(UUIDs.timeBased()); |
||||
|
Asset asset = new Asset(); |
||||
|
asset.setCustomerId(customerId); |
||||
|
|
||||
|
TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), new byte[4]); |
||||
|
|
||||
|
when(ctx.getAssetService()).thenReturn(assetService); |
||||
|
when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("wrong"))); |
||||
|
|
||||
|
node.onMsg(ctx, msg); |
||||
|
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctx).tellError(same(msg), captor.capture()); |
||||
|
Throwable value = captor.getValue(); |
||||
|
assertEquals("wrong", value.getMessage()); |
||||
|
} |
||||
|
|
||||
|
public void init(boolean startNewChain) throws TbNodeException { |
||||
|
TbChangeOriginatorNodeConfiguration config = new TbChangeOriginatorNodeConfiguration(); |
||||
|
config.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE); |
||||
|
config.setStartNewChain(startNewChain); |
||||
|
ObjectMapper mapper = new ObjectMapper(); |
||||
|
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(); |
||||
|
nodeConfiguration.setData(mapper.valueToTree(config)); |
||||
|
|
||||
|
node = new TbChangeOriginatorNode(); |
||||
|
node.init(nodeConfiguration, null); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,141 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2018 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.transform; |
||||
|
|
||||
|
import com.datastax.driver.core.utils.UUIDs; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Matchers; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.runners.MockitoJUnitRunner; |
||||
|
import org.mockito.stubbing.Answer; |
||||
|
import org.thingsboard.rule.engine.api.ListeningExecutor; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import java.util.concurrent.Callable; |
||||
|
|
||||
|
import static org.junit.Assert.assertEquals; |
||||
|
import static org.mockito.Matchers.same; |
||||
|
import static org.mockito.Mockito.*; |
||||
|
|
||||
|
@RunWith(MockitoJUnitRunner.class) |
||||
|
public class TbTransformMsgNodeTest { |
||||
|
|
||||
|
private TbTransformMsgNode node; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctx; |
||||
|
@Mock |
||||
|
private ListeningExecutor executor; |
||||
|
|
||||
|
@Test |
||||
|
public void metadataCanBeUpdated() throws TbNodeException { |
||||
|
initWithScript("meta.temp = meta.temp * 10;"); |
||||
|
TbMsgMetaData metaData = new TbMsgMetaData(); |
||||
|
metaData.putValue("temp", "7"); |
||||
|
metaData.putValue("humidity", "99"); |
||||
|
String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; |
||||
|
|
||||
|
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes()); |
||||
|
mockJsExecutor(); |
||||
|
|
||||
|
node.onMsg(ctx, msg); |
||||
|
verify(ctx).getJsExecutor(); |
||||
|
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctx).tellNext(captor.capture()); |
||||
|
TbMsg actualMsg = captor.getValue(); |
||||
|
assertEquals("70.0", actualMsg.getMetaData().getValue("temp")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void metadataCanBeAdded() throws TbNodeException { |
||||
|
initWithScript("meta.newAttr = meta.humidity - msg.passed;"); |
||||
|
TbMsgMetaData metaData = new TbMsgMetaData(); |
||||
|
metaData.putValue("temp", "7"); |
||||
|
metaData.putValue("humidity", "99"); |
||||
|
String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}"; |
||||
|
|
||||
|
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes()); |
||||
|
mockJsExecutor(); |
||||
|
|
||||
|
node.onMsg(ctx, msg); |
||||
|
verify(ctx).getJsExecutor(); |
||||
|
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctx).tellNext(captor.capture()); |
||||
|
TbMsg actualMsg = captor.getValue(); |
||||
|
assertEquals("94.0", actualMsg.getMetaData().getValue("newAttr")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void payloadCanBeUpdated() throws TbNodeException { |
||||
|
initWithScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' "); |
||||
|
TbMsgMetaData metaData = new TbMsgMetaData(); |
||||
|
metaData.putValue("temp", "7"); |
||||
|
metaData.putValue("humidity", "99"); |
||||
|
String rawJson = "{\"name\":\"Vit\",\"passed\": 5,\"bigObj\":{\"prop\":42}}"; |
||||
|
|
||||
|
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson.getBytes()); |
||||
|
mockJsExecutor(); |
||||
|
|
||||
|
node.onMsg(ctx, msg); |
||||
|
verify(ctx).getJsExecutor(); |
||||
|
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class); |
||||
|
verify(ctx).tellNext(captor.capture()); |
||||
|
TbMsg actualMsg = captor.getValue(); |
||||
|
String expectedJson = "{\"name\":\"Vit\",\"passed\":35.0,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}"; |
||||
|
assertEquals(expectedJson, new String(actualMsg.getData())); |
||||
|
} |
||||
|
|
||||
|
private void initWithScript(String script) throws TbNodeException { |
||||
|
TbTransformMsgNodeConfiguration config = new TbTransformMsgNodeConfiguration(); |
||||
|
config.setJsScript(script); |
||||
|
ObjectMapper mapper = new ObjectMapper(); |
||||
|
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(); |
||||
|
nodeConfiguration.setData(mapper.valueToTree(config)); |
||||
|
|
||||
|
node = new TbTransformMsgNode(); |
||||
|
node.init(nodeConfiguration, null); |
||||
|
} |
||||
|
|
||||
|
private void mockJsExecutor() { |
||||
|
when(ctx.getJsExecutor()).thenReturn(executor); |
||||
|
doAnswer((Answer<ListenableFuture<TbMsg>>) invocationOnMock -> { |
||||
|
try { |
||||
|
Callable task = (Callable) (invocationOnMock.getArguments())[0]; |
||||
|
return Futures.immediateFuture((TbMsg) task.call()); |
||||
|
} catch (Throwable th) { |
||||
|
return Futures.immediateFailedFuture(th); |
||||
|
} |
||||
|
}).when(executor).executeAsync(Matchers.any(Callable.class)); |
||||
|
} |
||||
|
|
||||
|
private void verifyError(TbMsg msg, String message, Class expectedClass) { |
||||
|
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctx).tellError(same(msg), captor.capture()); |
||||
|
|
||||
|
Throwable value = captor.getValue(); |
||||
|
assertEquals(expectedClass, value.getClass()); |
||||
|
assertEquals(message, value.getMessage()); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue