first commit

This commit is contained in:
zhou-hao 2020-01-14 11:25:37 +08:00
commit d854b56436
236 changed files with 14842 additions and 0 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root = true
[*]
charset = utf-8
[*.java]
indent_style = space
indent_size = tab
tab_width = 4
trim_trailing_whitespace = true
insert_final_newline = false

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
**/pom.xml.versionsBackup
**/target/
**/out/
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
.idea/
/nbproject
*.ipr
*.iws
*.iml
# Package Files #
*.jar
*.war
*.ear
*.log
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
**/transaction-logs/
!/.mvn/wrapper/maven-wrapper.jar
/data/
*.db
/static/
/upload
/ui/upload/
docker/data
!ip2region.db
!device-simulator.jar

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jetlinks-components</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>elasticsearch-component</artifactId>
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>org.hswebframework</groupId>
<artifactId>hsweb-easy-orm-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-commons-crud</artifactId>
<version>${hsweb.framework.version}</version>
</dependency>
<dependency>
<groupId>org.hswebframework</groupId>
<artifactId>hsweb-easy-orm-rdb</artifactId>
</dependency>
<dependency>
<groupId>org.jetlinks</groupId>
<artifactId>jetlinks-core</artifactId>
<version>${jetlinks.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package org.jetlinks.community.elastic.search;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
/**
* @author bsetfeng
* @since 1.0
**/
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class ElasticRestClient {
private RestHighLevelClient queryClient;
private RestHighLevelClient writeClient;
}

View File

@ -0,0 +1,118 @@
package org.jetlinks.community.elastic.search.aggreation;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.jetlinks.community.elastic.search.ElasticRestClient;
import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
import org.jetlinks.community.elastic.search.aggreation.bucket.BucketResponse;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponse;
import org.jetlinks.community.elastic.search.index.ElasticIndex;
import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
import org.jetlinks.community.elastic.search.service.AggregationService;
import org.jetlinks.community.elastic.search.service.IndexOperationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
/**
* @author bsetfeng
* @since 1.0
**/
@Service
@Slf4j
public class DefaultAggregationService implements AggregationService {
private final QueryParamTranslateService translateService;
private final ElasticRestClient restClient;
private final IndexOperationService indexOperationService;
@Autowired
public DefaultAggregationService(IndexOperationService indexOperationService,
ElasticRestClient restClient,
QueryParamTranslateService translateService) {
this.indexOperationService = indexOperationService;
this.restClient = restClient;
this.translateService = translateService;
}
@Override
public Mono<MetricsResponse> metricsAggregation(QueryParam queryParam,
MetricsAggregationStructure structure,
ElasticIndex provider) {
return searchSourceBuilderMono(queryParam, provider)
.doOnNext(builder -> builder.aggregation(
structure.getType().aggregationBuilder(structure.getName(), structure.getField())))
.map(builder -> new SearchRequest(provider.getStandardIndex())
.source(builder))
.flatMap(request -> Mono.<SearchResponse>create(monoSink ->
restClient.getQueryClient().searchAsync(request, RequestOptions.DEFAULT, translatorActionListener(monoSink))))
.map(searchResponse -> structure.getType().getResponse(structure.getName(), searchResponse));
}
@Override
public Mono<BucketResponse> bucketAggregation(QueryParam queryParam, BucketAggregationsStructure structure, ElasticIndex provider) {
return searchSourceBuilderMono(queryParam, provider)
.doOnNext(builder ->
builder.aggregation(structure.getType().aggregationBuilder(structure))
)
.map(builder -> new SearchRequest(provider.getStandardIndex())
.source(builder))
.doOnNext(searchRequest ->
log.debug("聚合查询index:{},参数:{}",
provider.getStandardIndex(),
JSON.toJSON(searchRequest.source().toString())))
.flatMap(request -> Mono.<SearchResponse>create(monoSink ->
restClient.getQueryClient().searchAsync(request, RequestOptions.DEFAULT, translatorActionListener(monoSink))))
.map(response -> structure.getType().convert(response.getAggregations().get(structure.getName())))
.map(buckets -> BucketResponse.builder()
.name(structure.getName())
.buckets(buckets)
.build()
)
;
}
private Mono<SearchSourceBuilder> searchSourceBuilderMono(QueryParam queryParam, ElasticIndex provider) {
queryParam.setPaging(false);
return indexOperationService.getIndexMappingMetadata(provider.getStandardIndex())
.map(metadata -> translateService.translate(queryParam, metadata))
.doOnError(e -> log.error("解析queryParam错误, index:{}", provider.getStandardIndex(), e));
// return Mono.just(translateService.translate(queryParam, IndexMappingMetadata.getInstance(provider.getStandardIndex())));
}
private <T> ActionListener<T> translatorActionListener(MonoSink<T> sink) {
return new ActionListener<T>() {
@Override
public void onResponse(T response) {
sink.success(response);
}
@Override
public void onFailure(Exception e) {
if (e instanceof ElasticsearchException) {
if (((ElasticsearchException) e).status().getStatus() == 404) {
sink.success();
return;
} else if (((ElasticsearchException) e).status().getStatus() == 400) {
sink.error(new ElasticsearchParseException("查询参数格式错误", e));
}
}
sink.error(e);
}
};
}
}

View File

@ -0,0 +1,154 @@
package org.jetlinks.community.elastic.search.aggreation.bucket;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.range.Range;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.avg.Avg;
import org.elasticsearch.search.aggregations.metrics.max.Max;
import org.elasticsearch.search.aggregations.metrics.min.Min;
import org.elasticsearch.search.aggregations.metrics.stats.Stats;
import org.elasticsearch.search.aggregations.metrics.sum.Sum;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author bsetfeng
* @since 1.0
**/
public class AggregationResponseHandle {
public static <A extends Aggregation> List<Bucket> terms(A a) {
Terms terms = (Terms) a;
return terms.getBuckets()
.stream()
.map(b -> {
Bucket bucket = Bucket.builder()
.key(b.getKeyAsString())
.count(b.getDocCount())
.build();
b.getAggregations().asList()
.forEach(subAggregation -> route(bucket, subAggregation));
return bucket;
}).collect(Collectors.toList())
;
}
public static <A extends Aggregation> List<Bucket> range(A a) {
Range range = (Range) a;
return range.getBuckets()
.stream()
.map(b -> {
Bucket bucket = Bucket.builder()
.key(b.getKeyAsString())
.from(b.getFrom())
.to(b.getTo())
.fromAsString(b.getFromAsString())
.toAsString(b.getToAsString())
.count(b.getDocCount()).build();
b.getAggregations().asList()
.forEach(subAggregation -> {
route(bucket, subAggregation);
});
return bucket;
}).collect(Collectors.toList())
;
}
public static <A extends Aggregation> List<Bucket> dateHistogram(A a) {
Histogram histogram = (Histogram) a;
return bucketsHandle(histogram.getBuckets());
}
private static List<Bucket> bucketsHandle(List<? extends Histogram.Bucket> buckets) {
return buckets
.stream()
.map(b -> {
Bucket bucket = Bucket.builder()
.key(b.getKeyAsString())
.count(b.getDocCount())
.build();
b.getAggregations().asList()
.forEach(subAggregation -> route(bucket, subAggregation));
return bucket;
}).collect(Collectors.toList())
;
}
private static <A extends Aggregation> void route(Bucket bucket, A a) {
if (a instanceof Terms) {
bucket.setBuckets(terms(a));
} else if (a instanceof Range) {
bucket.setBuckets(range(a));
} else if (a instanceof Histogram) {
bucket.setBuckets(range(a));
} else if (a instanceof Avg) {
bucket.setAvg(avg(a));
} else if (a instanceof Min) {
bucket.setMin(min(a));
} else if (a instanceof Max) {
bucket.setMax(max(a));
} else if (a instanceof Sum) {
bucket.setSum(sum(a));
} else if (a instanceof Stats) {
stats(bucket, a);
} else {
throw new UnsupportedOperationException("不支持的聚合类型");
}
}
public static <A extends Aggregation> MetricsResponseSingleValue avg(A a) {
Avg avg = (Avg) a;
return MetricsResponseSingleValue.builder()
.value(avg.getValue())
.valueAsString(avg.getValueAsString())
.build();
}
public static <A extends Aggregation> MetricsResponseSingleValue max(A a) {
Max max = (Max) a;
return MetricsResponseSingleValue.builder()
.value(max.getValue())
.valueAsString(max.getValueAsString())
.build();
}
public static <A extends Aggregation> MetricsResponseSingleValue min(A a) {
Min min = (Min) a;
return MetricsResponseSingleValue.builder()
.value(min.getValue())
.valueAsString(min.getValueAsString())
.build();
}
public static <A extends Aggregation> MetricsResponseSingleValue sum(A a) {
Sum sum = (Sum) a;
return MetricsResponseSingleValue.builder()
.value(sum.getValue())
.valueAsString(sum.getValueAsString())
.build();
}
public static <A extends Aggregation> void stats(Bucket bucket, A a) {
Stats stats = (Stats) a;
bucket.setAvg(MetricsResponseSingleValue.builder()
.value(stats.getAvg())
.valueAsString(stats.getAvgAsString())
.build());
bucket.setMax(MetricsResponseSingleValue.builder()
.value(stats.getMax())
.valueAsString(stats.getMaxAsString())
.build());
bucket.setMin(MetricsResponseSingleValue.builder()
.value(stats.getMin())
.valueAsString(stats.getMinAsString())
.build());
bucket.setSum(MetricsResponseSingleValue.builder()
.value(stats.getSum())
.valueAsString(stats.getSumAsString())
.build());
}
}

View File

@ -0,0 +1,42 @@
package org.jetlinks.community.elastic.search.aggreation.bucket;
import lombok.*;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
import java.util.List;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Bucket {
private String key;
private long count;
private String fromAsString;
private Object from;
private String toAsString;
private Object to;
private MetricsResponseSingleValue sum;
private MetricsResponseSingleValue valueCount;
private MetricsResponseSingleValue avg;
private MetricsResponseSingleValue min;
private MetricsResponseSingleValue max;
private List<Bucket> buckets;
}

View File

@ -0,0 +1,67 @@
package org.jetlinks.community.elastic.search.aggreation.bucket;
import lombok.*;
import org.hswebframework.utils.StringUtils;
import org.jetlinks.community.elastic.search.aggreation.enums.BucketType;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
import java.util.LinkedList;
import java.util.List;
/**
* @author bsetfeng
* @since 1.0
**/
@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BucketAggregationsStructure {
@NonNull
private String field;
private String name;
@NonNull
private BucketType type = BucketType.TERMS;
/**
* 指定返回分组数量
*/
private Integer size;
private Sort sort;
private List<Ranges> ranges;
/**
* 时间格式
*/
private String format;
/**
* 单位时间间隔
*
* @see DateHistogramInterval
*/
private String interval;
/**
* 缺失值
*/
private Object missingValue;
private List<MetricsAggregationStructure> subMetricsAggregation = new LinkedList<>();
private List<BucketAggregationsStructure> subBucketAggregation = new LinkedList<>();
public String getName() {
if (StringUtils.isNullOrEmpty(name)) {
name = type.name().concat("_").concat(field);
}
return name;
}
}

View File

@ -0,0 +1,21 @@
package org.jetlinks.community.elastic.search.aggreation.bucket;
import lombok.*;
import java.util.List;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BucketResponse {
private String name;
private List<Bucket> buckets;
}

View File

@ -0,0 +1,38 @@
package org.jetlinks.community.elastic.search.aggreation.bucket;
/**
* The interval the date histogram is based on.
*/
public class DateHistogramInterval {
public static final String SECOND = "1s";
public static final String MINUTE = "1m";
public static final String HOUR = "1h";
public static final String DAY = "1d";
public static final String WEEK = "1w";
public static final String MONTH = "1M";
public static final String QUARTER = "1q";
public static final String YEAR = "1y";
public static String seconds(int sec) {
return sec + "s";
}
public static String minutes(int min) {
return min + "m";
}
public static String hours(int hours) {
return hours + "h";
}
public static String days(int days) {
return days + "d";
}
public static String weeks(int weeks) {
return weeks + "w";
}
}

View File

@ -0,0 +1,19 @@
package org.jetlinks.community.elastic.search.aggreation.bucket;
import lombok.Getter;
import lombok.Setter;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@Setter
public class Ranges {
private String key;
private Object form;
private Object to;
}

View File

@ -0,0 +1,54 @@
package org.jetlinks.community.elastic.search.aggreation.bucket;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.community.elastic.search.aggreation.enums.OrderType;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@Setter
public class Sort {
private String order;
private OrderType type = OrderType.COUNT;
private Sort() {
}
private Sort(String order, OrderType type) {
this.type = type;
this.order = order;
}
private Sort(String order) {
this.order = order;
}
public String getOrder() {
if ("desc".equalsIgnoreCase(order)) {
return order;
} else {
return order = "asc";
}
}
public static Sort asc() {
return new Sort("asc");
}
public static Sort asc(OrderType type) {
return new Sort("asc", type);
}
public static Sort desc() {
return new Sort("desc");
}
public static Sort desc(OrderType type) {
return new Sort("desc", type);
}
}

View File

@ -0,0 +1,28 @@
package org.jetlinks.community.elastic.search.aggreation.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@AllArgsConstructor
public enum AggregationType {
AVG("平均"),
MAX("最大"),
COUNT("计数"),
MIN("最小"),
SUM("总数"),
STATS("统计汇总"),
EXTENDED_STATS("扩展统计"),
CARDINALITY("基数"),//去重统计
VALUE_COUNT("非空值计数"),
TERMS("字段项"),
RANGE("范围"),
DATE_HISTOGRAM("直方图"),
DATE_RANGE("时间范围");
private String text;
}

View File

@ -0,0 +1,182 @@
package org.jetlinks.community.elastic.search.aggreation.enums;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
import org.elasticsearch.search.aggregations.bucket.range.DateRangeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.jetlinks.community.elastic.search.aggreation.bucket.AggregationResponseHandle;
import org.jetlinks.community.elastic.search.aggreation.bucket.Bucket;
import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
import org.jetlinks.community.elastic.search.aggreation.bucket.Sort;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
import org.joda.time.DateTimeZone;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@AllArgsConstructor
public enum BucketType {
TERMS("字段项") {
@Override
public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
TermsAggregationBuilder builder = AggregationBuilders
.terms(structure.getName())
.field(structure.getField());
if (structure.getSize() != null) {
builder.size(structure.getSize());
}
Sort sort = structure.getSort();
if (sort != null) {
builder.order(mapping.get(OrderBuilder.of(sort.getOrder(), sort.getType())));
}
if (structure.getMissingValue() != null) {
builder.missing(structure.getMissingValue());
}
commonAggregationSetting(builder, structure);
return builder;
}
@Override
public <A extends Aggregation> List<Bucket> convert(A a) {
return AggregationResponseHandle.terms(a);
}
},
RANGE("范围") {
@Override
public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
RangeAggregationBuilder builder = AggregationBuilders
.range(structure.getName())
.field(structure.getField());
if (structure.getMissingValue() != null) {
builder.missing(structure.getMissingValue());
}
structure.getRanges()
.forEach(ranges -> {
builder.addRange(ranges.getKey(), (Double) ranges.getForm(), (Double) ranges.getTo());
});
commonAggregationSetting(builder, structure);
return builder;
}
@Override
public <A extends Aggregation> List<Bucket> convert(A a) {
return AggregationResponseHandle.range(a);
}
},
DATE_RANGE("时间范围") {
@Override
public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
DateRangeAggregationBuilder builder = AggregationBuilders
.dateRange(structure.getName())
.field(structure.getField());
if (StringUtils.hasText(structure.getFormat())) {
builder.format(structure.getFormat());
}
structure.getRanges()
.forEach(ranges -> {
builder.addRange(ranges.getKey(), ranges.getForm().toString(), ranges.getTo().toString());
});
if (structure.getMissingValue() != null) {
builder.missing(structure.getMissingValue());
}
builder.timeZone(DateTimeZone.getDefault());
commonAggregationSetting(builder, structure);
return builder;
}
@Override
public <A extends Aggregation> List<Bucket> convert(A a) {
return AggregationResponseHandle.range(a);
}
},
DATE_HISTOGRAM("时间范围") {
@Override
public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
DateHistogramAggregationBuilder builder = AggregationBuilders
.dateHistogram(structure.getName())
.field(structure.getField());
if (StringUtils.hasText(structure.getFormat())) {
builder.format(structure.getFormat());
}
if (StringUtils.hasText(structure.getInterval())) {
builder.dateHistogramInterval(new DateHistogramInterval(structure.getInterval()));
}
if (structure.getMissingValue() != null) {
builder.missing(structure.getMissingValue());
}
builder.timeZone(DateTimeZone.getDefault());
commonAggregationSetting(builder, structure);
return builder;
}
@Override
public <A extends Aggregation> List<Bucket> convert(A a) {
return AggregationResponseHandle.dateHistogram(a);
}
};
private String text;
public abstract AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure);
public abstract <A extends Aggregation> List<Bucket> convert(A a);
private static void commonAggregationSetting(AggregationBuilder builder, BucketAggregationsStructure structure) {
if (structure.getSubMetricsAggregation() != null && structure.getSubMetricsAggregation().size() > 0) {
addMetricsSubAggregation(builder, structure.getSubMetricsAggregation());
}
if (structure.getSubBucketAggregation() != null && structure.getSubBucketAggregation().size() > 0) {
addBucketSubAggregation(builder, structure.getSubBucketAggregation());
}
}
private static void addMetricsSubAggregation(AggregationBuilder builder, List<MetricsAggregationStructure> subMetricsAggregation) {
subMetricsAggregation
.forEach(subStructure -> {
builder.subAggregation(subStructure.getType().aggregationBuilder(subStructure.getName(), subStructure.getField()));
});
}
private static void addBucketSubAggregation(AggregationBuilder builder, List<BucketAggregationsStructure> subBucketAggregation) {
subBucketAggregation
.forEach(subStructure -> {
builder.subAggregation(subStructure.getType().aggregationBuilder(subStructure));
});
}
@Getter
@AllArgsConstructor(staticName = "of")
@EqualsAndHashCode
static class OrderBuilder {
private String order;
private OrderType orderType;
}
static Map<OrderBuilder, BucketOrder> mapping = new HashMap<>();
static {
mapping.put(OrderBuilder.of("asc", OrderType.COUNT), BucketOrder.count(true));
mapping.put(OrderBuilder.of("desc", OrderType.COUNT), BucketOrder.count(false));
mapping.put(OrderBuilder.of("asc", OrderType.KEY), BucketOrder.key(true));
mapping.put(OrderBuilder.of("desc", OrderType.KEY), BucketOrder.count(false));
}
}

View File

@ -0,0 +1,132 @@
package org.jetlinks.community.elastic.search.aggreation.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.metrics.avg.Avg;
import org.elasticsearch.search.aggregations.metrics.max.Max;
import org.elasticsearch.search.aggregations.metrics.min.Min;
import org.elasticsearch.search.aggregations.metrics.stats.Stats;
import org.elasticsearch.search.aggregations.metrics.sum.Sum;
import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCount;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponse;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@AllArgsConstructor
public enum MetricsType {
AVG("平均") {
@Override
public AggregationBuilder aggregationBuilder(String name, String filed) {
return AggregationBuilders.avg(name).field(filed);
}
@Override
public MetricsResponse getResponse(String name, SearchResponse response) {
Avg avg = response.getAggregations().get(name);
return MetricsResponse.builder()
.results(Collections.singletonMap(AVG,
new MetricsResponseSingleValue(avg.getValue(), avg.getValueAsString())))
.build();
}
},
MAX("最大") {
@Override
public AggregationBuilder aggregationBuilder(String name, String filed) {
return AggregationBuilders.max(name).field(filed);
}
@Override
public MetricsResponse getResponse(String name, SearchResponse response) {
Max max = response.getAggregations().get(name);
return MetricsResponse.builder()
.results(Collections.singletonMap(MAX,
new MetricsResponseSingleValue(max.getValue(), max.getValueAsString())))
.build();
}
},
VALUE_COUNT("非空值计数") {
@Override
public AggregationBuilder aggregationBuilder(String name, String filed) {
return AggregationBuilders.count(name).field(filed);
}
@Override
public MetricsResponse getResponse(String name, SearchResponse response) {
ValueCount valueCount = response.getAggregations().get(name);
return MetricsResponse.builder()
.results(Collections.singletonMap(VALUE_COUNT,
new MetricsResponseSingleValue(valueCount.getValue(), valueCount.getValueAsString())))
.build();
}
},
MIN("最小") {
@Override
public AggregationBuilder aggregationBuilder(String name, String filed) {
return AggregationBuilders.min(name).field(filed);
}
@Override
public MetricsResponse getResponse(String name, SearchResponse response) {
Min min = response.getAggregations().get(name);
return MetricsResponse.builder()
.results(Collections.singletonMap(MIN,
new MetricsResponseSingleValue(min.getValue(), min.getValueAsString())))
.build();
}
},
SUM("总数") {
@Override
public AggregationBuilder aggregationBuilder(String name, String filed) {
return AggregationBuilders.sum(name).field(filed);
}
@Override
public MetricsResponse getResponse(String name, SearchResponse response) {
Sum sum = response.getAggregations().get(name);
return MetricsResponse.builder()
.results(Collections.singletonMap(SUM,
new MetricsResponseSingleValue(sum.getValue(), sum.getValueAsString())))
.build();
}
},
STATS("统计汇总") {
@Override
public AggregationBuilder aggregationBuilder(String name, String filed) {
return AggregationBuilders.stats(name).field(filed);
}
@Override
public MetricsResponse getResponse(String name, SearchResponse response) {
Stats stats = response.getAggregations().get(name);
Map<MetricsType, MetricsResponseSingleValue> results = new HashMap<>();
results.put(AVG, new MetricsResponseSingleValue(stats.getAvg(), stats.getAvgAsString()));
results.put(MIN, new MetricsResponseSingleValue(stats.getMin(), stats.getMinAsString()));
results.put(MAX, new MetricsResponseSingleValue(stats.getMax(), stats.getMaxAsString()));
results.put(SUM, new MetricsResponseSingleValue(stats.getSum(), stats.getMaxAsString()));
results.put(VALUE_COUNT, new MetricsResponseSingleValue(stats.getCount(), String.valueOf(stats.getCount())));
return MetricsResponse.builder()
.results(results)
.build();
}
};
private String text;
public abstract AggregationBuilder aggregationBuilder(String name, String filed);
public abstract MetricsResponse getResponse(String name, SearchResponse response);
}

View File

@ -0,0 +1,18 @@
package org.jetlinks.community.elastic.search.aggreation.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author bsetfeng
* @since 1.0
**/
@AllArgsConstructor
@Getter
public enum OrderType {
COUNT("计数"),
KEY("分组值");
private String text;
}

View File

@ -0,0 +1,37 @@
package org.jetlinks.community.elastic.search.aggreation.metrics;
import lombok.*;
import org.hswebframework.utils.StringUtils;
import org.jetlinks.community.elastic.search.aggreation.enums.MetricsType;
/**
* @author bsetfeng
* @since 1.0
**/
@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MetricsAggregationStructure {
@NonNull
private String field;
private String name;
@NonNull
private MetricsType type = MetricsType.VALUE_COUNT;
/**
* 缺失值
*/
private Object missingValue;
public String getName() {
if (StringUtils.isNullOrEmpty(name)) {
name = type.name().concat("_").concat(field);
}
return name;
}
}

View File

@ -0,0 +1,33 @@
package org.jetlinks.community.elastic.search.aggreation.metrics;
import lombok.*;
import org.jetlinks.community.elastic.search.aggreation.enums.MetricsType;
import java.util.Map;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MetricsResponse {
private Map<MetricsType, MetricsResponseSingleValue> results;
private MetricsResponseSingleValue singleResult;
public MetricsResponseSingleValue getSingleResult() {
if (singleResult == null) {
this.singleResult = results.entrySet()
.stream()
.findFirst()
.map(Map.Entry::getValue)
.orElse(MetricsResponseSingleValue.empty());
}
return singleResult;
}
}

View File

@ -0,0 +1,45 @@
package org.jetlinks.community.elastic.search.aggreation.metrics;
import lombok.*;
/**
* @author bsetfeng
* @since 1.0
**/
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MetricsResponseSingleValue {
private double value;
private String valueAsString;
public static MetricsResponseSingleValue empty() {
return new MetricsResponseSingleValue();
}
// public double getValue() {
// if (isDoubleOrFloat(String.valueOf(value))) {
// BigDecimal b = BigDecimal.valueOf(value);
// return b.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
// } else {
// return 0;
// }
// }
//
// public static boolean isDoubleOrFloat(String str) {
// if (str.length() > 15) {
// str = str.substring(0, 15);
// }
// Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
// return pattern.matcher(str).matches();
// }
//
// public static void main(String[] args) {
// System.out.println(new BigDecimal("8.839927333333333E7"));
// }
}

View File

@ -0,0 +1,35 @@
package org.jetlinks.community.elastic.search.configuration;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.jetlinks.community.elastic.search.ElasticRestClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author bsetfeng
* @since 1.0
**/
@Configuration
@Slf4j
@EnableConfigurationProperties(ElasticSearchProperties.class)
public class ElasticSearchConfiguration {
@Autowired
private ElasticSearchProperties properties;
@Bean
public ElasticRestClient elasticRestClient() {
RestHighLevelClient queryClient = new RestHighLevelClient(RestClient.builder(properties.createHosts())
.setRequestConfigCallback(properties::applyRequestConfigBuilder)
.setHttpClientConfigCallback(properties::applyHttpAsyncClientBuilder));
RestHighLevelClient writeClient = new RestHighLevelClient(RestClient.builder(properties.createHosts())
.setRequestConfigCallback(properties::applyRequestConfigBuilder)
.setHttpClientConfigCallback(properties::applyHttpAsyncClientBuilder));
return new ElasticRestClient(queryClient, writeClient);
}
}

View File

@ -0,0 +1,51 @@
package org.jetlinks.community.elastic.search.configuration;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections.CollectionUtils;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@ConfigurationProperties(prefix = "elasticsearch.client")
@Getter
@Setter
public class ElasticSearchProperties {
private String host = "localhost";
private int port = 9200;
private int connectionRequestTimeout = 5000;
private int connectTimeout = 2000;
private int socketTimeout = 2000;
private int maxConnTotal = 30;
private List<String> hosts;
public HttpHost[] createHosts() {
if (CollectionUtils.isEmpty(hosts)) {
return new HttpHost[]{new HttpHost(host, port, "http")};
}
return hosts.stream().map(HttpHost::create).toArray(HttpHost[]::new);
}
public RequestConfig.Builder applyRequestConfigBuilder(RequestConfig.Builder builder) {
builder.setConnectTimeout(connectTimeout);
builder.setConnectionRequestTimeout(connectionRequestTimeout);
builder.setSocketTimeout(socketTimeout);
return builder;
}
public HttpAsyncClientBuilder applyHttpAsyncClientBuilder(HttpAsyncClientBuilder builder) {
builder.setMaxConnTotal(maxConnTotal);
return builder;
}
}

View File

@ -0,0 +1,41 @@
package org.jetlinks.community.elastic.search.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.web.dict.EnumDict;
import java.util.List;
/**
* @author bsetfeng
* @since 1.0
* Values based on reference doc - https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html
**/
@Getter
@AllArgsConstructor
public enum FieldDateFormat implements EnumDict<String> {
epoch_millis("epoch_millis", "毫秒"),
epoch_second("epoch_second", ""),
strict_date("strict_date", "yyyy-MM-dd"),
basic_date_time("basic_date_time", "yyyyMMdd'T'HHmmss.SSSZ"),
strict_date_time("strict_date_time", "yyyy-MM-dd'T'HH:mm:ss.SSSZZ"),
strict_date_hour_minute_second("strict_date_hour_minute_second", "yyyy-MM-dd'T'HH:mm:ss"),
strict_hour_minute_second("strict_hour_minute_second", "HH:mm:ss"),
simple_date("yyyy-MM-dd HH:mm:ss", "通用格式");
private String value;
private String text;
public static String getFormatStr(List<FieldDateFormat> dateFormats) {
StringBuffer format = new StringBuffer();
for (int i = 0; i < dateFormats.size(); i++) {
format.append(dateFormats.get(i).getValue());
if (i != dateFormats.size() - 1) {
format.append("||");
}
}
return format.toString();
}
}

View File

@ -0,0 +1,48 @@
package org.jetlinks.community.elastic.search.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.exception.NotFoundException;
import org.springframework.util.StringUtils;
@AllArgsConstructor
public enum FieldType implements EnumDict<String> {
TEXT("text"),
BYTE("byte"),
SHORT("short"),
INTEGER("integer"),
LONG("long"),
DATE("date"),
HALF_FLOAT("half_float"),
FLOAT("float"),
DOUBLE("double"),
BOOLEAN("boolean"),
OBJECT("object"),
AUTO("auto"),
NESTED("nested"),
IP("ip"),
ATTACHMENT("attachment"),
KEYWORD("keyword");
@Override
public String getText() {
return value;
}
@Getter
private String value;
public static FieldType of(Object value) {
if (!StringUtils.isEmpty(value)) {
for (FieldType fieldType : FieldType.values()) {
if (fieldType.getValue().equals(value)) {
return fieldType;
}
}
}
throw new NotFoundException("未找到数据类型为:" + value + "的枚举");
}
}

View File

@ -0,0 +1,69 @@
package org.jetlinks.community.elastic.search.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.hswebframework.ezorm.core.param.Term;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Optional;
/**
* @author Jia_RG
*/
@Getter
@AllArgsConstructor
public enum LinkTypeEnum {
and("and") {
@Override
public void process(BoolQueryBuilder query, Term term) {
if (term.getTerms().isEmpty()) {
query.must(TermTypeEnum.of(term.getTermType().trim()).map(e -> e.process(term)).orElse(QueryBuilders.boolQuery()));
} else {
// 嵌套查询新建一个包起来
BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
// 同一层级取最后一个的type
LinkTypeEnum.of(getLast(terms).getType().name()).ifPresent(e -> terms.forEach(t -> e.process(nextQuery, t)));
// 处理完后包括进去
query.must(nextQuery);
}
}
},
or("or") {
@Override
public void process(BoolQueryBuilder query, Term term) {
// 跟上面代码相似
if (term.getTerms().isEmpty()) {
query.should(TermTypeEnum.of(term.getTermType().trim()).map(e -> e.process(term)).orElse(QueryBuilders.boolQuery()));
} else {
BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
LinkTypeEnum.of(getLast(terms).getType().name()).ifPresent(e -> terms.forEach(t -> e.process(nextQuery, t)));
query.should(nextQuery);
}
}
};
private final String type;
public abstract void process(BoolQueryBuilder query, Term term);
public static Optional<LinkTypeEnum> of(String type) {
return Arrays.stream(values())
.filter(e -> e.getType().equalsIgnoreCase(type))
.findAny();
}
private static Term getLast(LinkedList<Term> terms) {
int index = terms.indexOf(terms.getLast());
while (index >= 0) {
if (terms.get(index).getTerms().isEmpty()) break;
index--;
}
return terms.get(index);
}
}

View File

@ -0,0 +1,109 @@
package org.jetlinks.community.elastic.search.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.hswebframework.ezorm.core.param.Term;
import org.jetlinks.community.elastic.search.utils.TermCommonUtils;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* @author Jia_RG
* @author bestfeng
*/
@Getter
@AllArgsConstructor
public enum TermTypeEnum {
eq("eq") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.termQuery(term.getColumn().trim(), term.getValue());
}
},
not("not") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.boolQuery().mustNot(QueryBuilders.termQuery(term.getColumn().trim(), term.getValue()));
}
},
btw("btw") {
@Override
public QueryBuilder process(Term term) {
Object between = null;
Object and = null;
List values = TermCommonUtils.convertToList(term.getValue());
if (values.size() > 0) {
between = values.get(0);
}
if (values.size() > 1) {
and = values.get(1);
}
return QueryBuilders.rangeQuery(term.getColumn().trim()).gte(between).lte(and);
}
},
gt("gt") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.rangeQuery(term.getColumn().trim()).gt(term.getValue());
}
},
gte("gte") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.rangeQuery(term.getColumn().trim()).gte(term.getValue());
}
},
lt("lt") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.rangeQuery(term.getColumn().trim()).lt(term.getValue());
}
},
lte("lte") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.rangeQuery(term.getColumn().trim()).lte(term.getValue());
}
},
in("in") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.termsQuery(term.getColumn().trim(), TermCommonUtils.convertToList(term.getValue()));
}
},
like("like") {
@Override
public QueryBuilder process(Term term) {
//return QueryBuilders.matchPhraseQuery(term.getColumn().trim(), term.getValue());
return QueryBuilders.wildcardQuery(term.getColumn().trim(), likeQueryTermValueHandler(term.getValue()));
}
},
nlike("nlike") {
@Override
public QueryBuilder process(Term term) {
return QueryBuilders.boolQuery().mustNot(QueryBuilders.wildcardQuery(term.getColumn().trim(), likeQueryTermValueHandler(term.getValue())));
}
};
private final String type;
public abstract QueryBuilder process(Term term);
public static String likeQueryTermValueHandler(Object value) {
if (!StringUtils.isEmpty(value)) {
return value.toString().replace("%", "*");
}
return "**";
}
public static Optional<TermTypeEnum> of(String type) {
return Arrays.stream(values())
.filter(e -> e.getType().equalsIgnoreCase(type))
.findAny();
}
}

View File

@ -0,0 +1,68 @@
package org.jetlinks.community.elastic.search.index;
import lombok.Getter;
import lombok.Setter;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.jetlinks.community.elastic.search.index.mapping.MappingFactory;
import org.jetlinks.community.elastic.search.index.setting.SettingFactory;
import java.util.Collections;
import java.util.Map;
/**
* @author bsetfeng
* @since 1.0
**/
public class CreateIndex {
@Getter
@Setter
private Map<String, Object> mapping;
@Getter
@Setter
private Settings.Builder settings;
private String index;
@Deprecated
private String type;
public CreateIndex addIndex(String index) {
this.index = index;
return this;
}
@Deprecated
public CreateIndex addType(String type) {
this.type = type;
return this;
}
public MappingFactory createMapping() {
return MappingFactory.createInstance(this);
}
public SettingFactory createSettings() {
return SettingFactory.createInstance(this);
}
public CreateIndexRequest createIndexRequest() {
CreateIndexRequest request = new CreateIndexRequest(index);
request.mapping(Collections.singletonMap("properties", getMapping()));
if (settings != null) {
request.settings(settings);
}
return request;
}
private CreateIndex() {
}
public static CreateIndex createInstance() {
return new CreateIndex();
}
}

View File

@ -0,0 +1,128 @@
package org.jetlinks.community.elastic.search.index;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.indices.*;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.elastic.search.ElasticRestClient;
import org.jetlinks.community.elastic.search.enums.FieldType;
import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
import org.jetlinks.community.elastic.search.index.mapping.IndicesMappingCenter;
import org.jetlinks.community.elastic.search.index.mapping.SingleMappingMetadata;
import org.jetlinks.community.elastic.search.service.IndexOperationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* @author bsetfeng
* @since 1.0
**/
@Service
@Slf4j
public class DefaultIndexOperationService implements IndexOperationService {
private final ElasticRestClient restClient;
private final IndicesMappingCenter indicesMappingCenter;
@Autowired
public DefaultIndexOperationService(ElasticRestClient restClient, IndicesMappingCenter indicesMappingCenter) {
this.restClient = restClient;
this.indicesMappingCenter = indicesMappingCenter;
}
@Override
public Mono<Boolean> indexIsExists(String index) {
return Mono.create(sink -> {
try {
GetIndexRequest request = new GetIndexRequest(index);
sink.success(restClient.getQueryClient().indices().exists(request, RequestOptions.DEFAULT));
} catch (Exception e) {
log.error("查询es index 是否存在失败", e);
sink.error(e);
}
});
}
@Override
public Mono<Boolean> init(CreateIndexRequest request) {
return indexIsExists(request.index())
.filter(bool -> !bool)
.flatMap(b -> Mono.create(sink -> {
restClient.getQueryClient().indices().createAsync(request, RequestOptions.DEFAULT, new ActionListener<CreateIndexResponse>() {
@Override
public void onResponse(CreateIndexResponse createIndexResponse) {
sink.success(createIndexResponse.isAcknowledged());
}
@Override
public void onFailure(Exception e) {
sink.error(e);
}
});
}));
}
@Override
public Mono<IndexMappingMetadata> getIndexMappingMetadata(String index) {
return indicesMappingCenter.getIndexMappingMetaData(index)
.map(Mono::just).orElseGet(() ->
getIndexMapping(index)
.doOnNext(indicesMappingCenter::register));
}
private Mono<IndexMappingMetadata> getIndexMapping(String index) {
return indexIsExists(index)
.filter(Boolean::booleanValue)
.flatMap(bool -> Mono.create(sink -> {
if (bool) {
GetMappingsRequest mappingsRequest = new GetMappingsRequest();
mappingsRequest.indices(index);
restClient.getQueryClient().indices().getMappingAsync(mappingsRequest, RequestOptions.DEFAULT, new ActionListener<GetMappingsResponse>() {
@Override
public void onResponse(GetMappingsResponse getMappingsResponse) {
//index存在时 getMappingsResponse.mappings().get(index).getSourceAsMap().get("properties") 不会为空
sink.success(fieldMappingConvert(null, IndexMappingMetadata.getInstance(index), getMappingsResponse.mappings().get(index).getSourceAsMap().get("properties")));
}
@Override
public void onFailure(Exception e) {
sink.error(e);
}
});
}
}));
}
private IndexMappingMetadata fieldMappingConvert(String baseKey, IndexMappingMetadata indexMappingMetaData, Object properties) {
FastBeanCopier.copy(properties, new HashMap<String, Object>())
.forEach((key, value) -> {
if (StringUtils.hasText(baseKey)) {
key = baseKey.concat(".").concat(key);
}
if (value instanceof Map) {
Map tempValue = FastBeanCopier.copy(value, new HashMap<>());
Object childProperties = tempValue.get("properties");
if (childProperties != null) {
fieldMappingConvert(key, indexMappingMetaData, childProperties);
return;
}
indexMappingMetaData.setMetadata(SingleMappingMetadata.builder()
.name(key)
.type(FieldType.of(tempValue.get("type")))
.build());
}
});
return indexMappingMetaData;
}
}

View File

@ -0,0 +1,37 @@
package org.jetlinks.community.elastic.search.index;
import java.util.function.Supplier;
/**
* @version 1.0
**/
public interface ElasticIndex {
@Deprecated
String getIndex();
@Deprecated
String getType();
default String getStandardIndex(){
return getIndex().toLowerCase();
}
default String getStandardType(){
return getType().toLowerCase();
}
static ElasticIndex createDefaultIndex(Supplier<String> indexConsumer, Supplier<String> typeConsumer) {
return new ElasticIndex() {
@Override
public String getIndex() {
return indexConsumer.get();
}
@Override
public String getType() {
return typeConsumer.get();
}
};
}
}

View File

@ -0,0 +1,64 @@
package org.jetlinks.community.elastic.search.index.mapping;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.community.elastic.search.enums.FieldType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@Setter
public class IndexMappingMetadata {
private String index;
private Map<String, SingleMappingMetadata> metadata = new HashMap<>();
public List<SingleMappingMetadata> getAllMetaData() {
return metadata.entrySet()
.stream()
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}
public SingleMappingMetadata getMetaData(String fieldName) {
return metadata.get(fieldName);
}
public List<SingleMappingMetadata> getMetaDataByType(FieldType type) {
return getAllMetaData()
.stream()
.filter(singleMapping -> singleMapping.getType().equals(type))
.collect(Collectors.toList());
}
public Map<String, SingleMappingMetadata> getMetaDataByTypeToMap(FieldType type) {
return getMetaDataByType(type)
.stream()
.collect(Collectors.toMap(SingleMappingMetadata::getName, Function.identity()));
}
public void setMetadata(SingleMappingMetadata singleMapping) {
metadata.put(singleMapping.getName(), singleMapping);
}
private IndexMappingMetadata(String index) {
this.index = index;
}
private IndexMappingMetadata() {
}
public static IndexMappingMetadata getInstance(String index) {
return new IndexMappingMetadata(index);
}
}

View File

@ -0,0 +1,26 @@
package org.jetlinks.community.elastic.search.index.mapping;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author bsetfeng
* @since 1.0
**/
@Component
public class IndicesMappingCenter {
private Map<String, IndexMappingMetadata> indicesMapping = new ConcurrentHashMap<>();
public Optional<IndexMappingMetadata> getIndexMappingMetaData(String index) {
return Optional.ofNullable(indicesMapping.get(index));
}
public void register(IndexMappingMetadata mappingMetaData) {
indicesMapping.put(mappingMetaData.getIndex(), mappingMetaData);
}
}

View File

@ -0,0 +1,71 @@
package org.jetlinks.community.elastic.search.index.mapping;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.elastic.search.enums.FieldType;
import org.jetlinks.community.elastic.search.enums.FieldDateFormat;
import org.jetlinks.community.elastic.search.index.CreateIndex;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* @author bsetfeng
* @since 1.0
**/
public class MappingFactory {
private Map<String, Object> properties = new HashMap<>();
private Map<String, Object> filedMap = new HashMap<>();
private volatile boolean flag = true;
private CreateIndex index;
private MappingFactory(CreateIndex index) {
this.index = index;
}
public MappingFactory addFieldName(String fieldName) {
continuityOperateHandle(!flag);
flag = false;
filedMap = new HashMap<>();
properties.put(fieldName, filedMap);
return this;
}
public MappingFactory addFieldType(FieldType type) {
continuityOperateHandle(flag);
filedMap.put("type", type.getValue());
return this;
}
public MappingFactory addFieldDateFormat(FieldDateFormat... dateFormats) {
continuityOperateHandle(flag);
filedMap.put("format", FieldDateFormat.getFormatStr(Arrays.asList(dateFormats)));
return this;
}
public MappingFactory commit() {
flag = true;
return this;
}
public CreateIndex end() {
index.setMapping(properties);
return index;
}
public static MappingFactory createInstance(CreateIndex index) {
return new MappingFactory(index);
}
private void continuityOperateHandle(boolean inoperable) {
if (inoperable) {
throw new BusinessException("please exec commit() or addFiledName() later then operate");
}
}
}

View File

@ -0,0 +1,23 @@
package org.jetlinks.community.elastic.search.index.mapping;
import lombok.*;
import org.jetlinks.community.elastic.search.enums.FieldType;
import org.jetlinks.community.elastic.search.enums.FieldDateFormat;
/**
* @author bsetfeng
* @since 1.0
**/
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SingleMappingMetadata {
private String name;
private FieldDateFormat format;
private FieldType type;
}

View File

@ -0,0 +1,40 @@
package org.jetlinks.community.elastic.search.index.setting;
import org.elasticsearch.common.settings.Settings;
import org.jetlinks.community.elastic.search.index.CreateIndex;
/**
* @author bsetfeng
* @since 1.0
**/
public class SettingFactory {
private Settings.Builder settings = Settings.builder();
private CreateIndex index;
private SettingFactory(CreateIndex index) {
this.index = index;
}
public SettingFactory settingShards(Integer shards) {
settings.put("number_of_shards", shards);
return this;
}
public SettingFactory settingReplicas(Integer replicas) {
settings.put("number_of_replicas", replicas);
return this;
}
public CreateIndex end() {
index.setSettings(settings);
return index;
}
public static SettingFactory createInstance(CreateIndex index) {
return new SettingFactory(index);
}
}

View File

@ -0,0 +1,35 @@
package org.jetlinks.community.elastic.search.parser;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
import org.springframework.util.StringUtils;
/**
* @author bsetfeng
* @since 1.0
**/
public abstract class AbstractQueryParamTranslateService implements QueryParamTranslateService {
@Override
public SearchSourceBuilder translate(QueryParam queryParam, IndexMappingMetadata mappingMetaData) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
if (queryParam.isPaging()) {
sourceBuilder.from(queryParam.getPageIndex() * queryParam.getPageSize());
sourceBuilder.size(queryParam.getPageSize());
}
queryParam.getSorts()
.forEach(sort -> {
if (!StringUtils.isEmpty(sort.getName())) {
sourceBuilder.sort(sort.getName(), SortOrder.fromString(sort.getOrder()));
}
});
return sourceBuilder.query(queryBuilder(queryParam, mappingMetaData));
}
protected abstract QueryBuilder queryBuilder(QueryParam queryParam, IndexMappingMetadata mappingMetaData);
}

View File

@ -0,0 +1,64 @@
package org.jetlinks.community.elastic.search.parser;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.hswebframework.ezorm.core.param.Term;
import org.springframework.stereotype.Component;
import java.util.LinkedList;
import java.util.function.Consumer;
/**
* @author bsetfeng
* @since 1.0
**/
@Component
public class DefaultLinkTypeParser implements LinkTypeParser {
private TermTypeParser parser = new DefaultTermTypeParser();
@Override
public BoolQueryBuilder process(Term term, Consumer<Term> consumer, BoolQueryBuilder queryBuilders) {
if ("or".equalsIgnoreCase(term.getType().name())) {
handleOr(queryBuilders, term, consumer);
} else if ("and".equalsIgnoreCase(term.getType().name())) {
handleAnd(queryBuilders, term, consumer);
} else {
throw new UnsupportedOperationException("不支持的查询连接类型,term.getType:" + term.getType().name());
}
return queryBuilders;
}
private void handleOr(BoolQueryBuilder queryBuilders, Term term, Consumer<Term> consumer) {
consumer.accept(term);
if (term.getTerms().isEmpty()) {
parser.process(() -> term, queryBuilders::should);
} else {
BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
terms.forEach(t -> process(t, consumer, nextQuery));
queryBuilders.should(nextQuery);
}
}
private void handleAnd(BoolQueryBuilder queryBuilders, Term term, Consumer<Term> consumer) {
consumer.accept(term);
if (term.getTerms().isEmpty()) {
parser.process(() -> term, queryBuilders::must);
} else {
BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
terms.forEach(t -> process(t, consumer, nextQuery));
queryBuilders.must(nextQuery);
}
}
private static Term getLast(LinkedList<Term> terms) {
int index = terms.indexOf(terms.getLast());
while (index >= 0) {
if (terms.get(index).getTerms().isEmpty()) break;
index--;
}
return terms.get(index);
}
}

View File

@ -0,0 +1,52 @@
package org.jetlinks.community.elastic.search.parser;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.hswebframework.ezorm.core.param.Term;
import org.jetlinks.community.elastic.search.enums.FieldType;
import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
import org.jetlinks.community.elastic.search.index.mapping.SingleMappingMetadata;
import org.jetlinks.community.elastic.search.utils.DateTimeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author bsetfeng
* @since 1.0
**/
@Component
public class DefaultQueryParamTranslateService extends AbstractQueryParamTranslateService {
private final LinkTypeParser parser;
@Value("${jetlinks.system.formats:yyyy-MM-dd HH:mm:ss}")
private List<String> formats;
@Autowired
public DefaultQueryParamTranslateService(LinkTypeParser parser) {
this.parser = parser;
}
@Override
protected QueryBuilder queryBuilder(QueryParam queryParam, IndexMappingMetadata mappingMetaData) {
BoolQueryBuilder queryBuilders = QueryBuilders.boolQuery();
queryParam.getTerms()
.forEach(term -> parser.process(term, t ->
dateTypeHandle(t, mappingMetaData.getMetaData(t.getColumn())), queryBuilders));
return queryBuilders;
}
private void dateTypeHandle(Term term, SingleMappingMetadata singleMappingMetaData) {
if (singleMappingMetaData == null) return;
if (singleMappingMetaData.getType().equals(FieldType.DATE)) {
term.setValue(DateTimeUtils.formatDateToTimestamp(term.getValue(), formats));
}
}
}

View File

@ -0,0 +1,28 @@
package org.jetlinks.community.elastic.search.parser;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.hswebframework.ezorm.core.param.Term;
import org.jetlinks.community.elastic.search.enums.TermTypeEnum;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @author bsetfeng
* @since 1.0
**/
public class DefaultTermTypeParser implements TermTypeParser {
@Override
public void process(Supplier<Term> termSupplier, Function<QueryBuilder, BoolQueryBuilder> function) {
function.apply(queryBuilder(termSupplier.get()));
}
private QueryBuilder queryBuilder(Term term) {
return TermTypeEnum.of(term.getTermType().trim()).map(e -> e.process(term)).orElse(QueryBuilders.boolQuery());
}
}

View File

@ -0,0 +1,14 @@
package org.jetlinks.community.elastic.search.parser;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.hswebframework.ezorm.core.param.Term;
import java.util.function.Consumer;
/**
* @version 1.0
**/
public interface LinkTypeParser {
BoolQueryBuilder process(Term term, Consumer<Term> consumer, BoolQueryBuilder queryBuilders);
}

View File

@ -0,0 +1,14 @@
package org.jetlinks.community.elastic.search.parser;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
/**
* @author bsetfeng
* @since 1.0
**/
public interface QueryParamTranslateService {
SearchSourceBuilder translate(QueryParam queryParam, IndexMappingMetadata metaData);
}

View File

@ -0,0 +1,17 @@
package org.jetlinks.community.elastic.search.parser;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.hswebframework.ezorm.core.param.Term;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @version 1.0
**/
public interface TermTypeParser {
void process(Supplier<Term> termSupplier, Function<QueryBuilder, BoolQueryBuilder> function);
}

View File

@ -0,0 +1,20 @@
package org.jetlinks.community.elastic.search.service;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
import org.jetlinks.community.elastic.search.aggreation.bucket.BucketResponse;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponse;
import org.jetlinks.community.elastic.search.index.ElasticIndex;
import reactor.core.publisher.Mono;
/**
* @author bsetfeng
* @since 1.0
**/
public interface AggregationService {
Mono<MetricsResponse> metricsAggregation(QueryParam queryParam, MetricsAggregationStructure structure, ElasticIndex provider);
Mono<BucketResponse> bucketAggregation(QueryParam queryParam, BucketAggregationsStructure structure, ElasticIndex provider);
}

View File

@ -0,0 +1,251 @@
package org.jetlinks.community.elastic.search.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.core.CountRequest;
import org.elasticsearch.client.core.CountResponse;
import org.elasticsearch.common.xcontent.XContentType;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.hswebframework.web.api.crud.entity.PagerResult;
import org.jetlinks.community.elastic.search.ElasticRestClient;
import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
import org.jetlinks.core.utils.FluxUtils;
import org.jetlinks.community.elastic.search.index.ElasticIndex;
import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author bsetfeng
* @since 1.0
**/
@Service
@Slf4j
public class DefaultElasticSearchService implements ElasticSearchService {
private final ElasticRestClient restClient;
private final IndexOperationService indexOperationService;
private final QueryParamTranslateService translateService;
FluxSink<Buffer> sink;
@Autowired
public DefaultElasticSearchService(ElasticRestClient restClient,
QueryParamTranslateService translateService,
IndexOperationService indexOperationService) {
this.restClient = restClient;
this.translateService = translateService;
this.indexOperationService = indexOperationService;
}
@Override
public <T> Mono<PagerResult<T>> queryPager(ElasticIndex index, QueryParam queryParam, Class<T> type) {
return query(searchRequestStructure(queryParam, index))
.map(response -> translatePageResult(type, queryParam, response))
.switchIfEmpty(Mono.just(PagerResult.empty()));
}
@Override
public <T> Flux<T> query(ElasticIndex index, QueryParam queryParam, Class<T> type) {
return query(searchRequestStructure(queryParam, index))
.flatMapIterable(response -> translate(type, response));
}
@Override
public Mono<Long> count(ElasticIndex index, QueryParam queryParam) {
return countQuery(countRequestStructure(queryParam, index))
.map(CountResponse::getCount)
.switchIfEmpty(Mono.just(0L));
}
@Override
public <T> Mono<Void> commit(ElasticIndex index, T payload) {
return Mono.fromRunnable(() -> {
sink.next(new Buffer(index, payload));
});
}
@Override
public <T> Mono<Void> commit(ElasticIndex index, Collection<T> payload) {
return Mono.fromRunnable(() -> {
for (T t : payload) {
sink.next(new Buffer(index, t));
}
});
}
@PreDestroy
public void shutdown() {
sink.complete();
}
@PostConstruct
public void init() {
FluxUtils.bufferRate(Flux.<Buffer>create(sink -> this.sink = sink),
1000, 2000, Duration.ofSeconds(3))
.flatMap(this::doSave)
.doOnNext((len) ->{
//System.out.println(len);
log.debug("保存ES数据成功:{}", len);
})
.onErrorContinue((err, obj) -> {
//打印到控制台以免递归调用ES导致崩溃
System.err.println(org.hswebframework.utils.StringUtils.throwable2String(err));
})
.subscribe();
}
@AllArgsConstructor
@Getter
static class Buffer {
ElasticIndex index;
Object payload;
}
protected Mono<Integer> doSave(Collection<Buffer> buffers) {
return Flux.fromIterable(buffers)
.collect(Collectors.groupingBy(Buffer::getIndex))
.map(Map::entrySet)
.flatMapIterable(Function.identity())
.map(entry -> {
ElasticIndex index = entry.getKey();
BulkRequest bulkRequest = new BulkRequest(index.getStandardIndex(), index.getStandardType());
for (Buffer buffer : entry.getValue()) {
IndexRequest request = new IndexRequest();
Object o = JSON.toJSON(buffer.getPayload());
if (o instanceof Map) {
request.source((Map) o);
} else {
request.source(o.toString(), XContentType.JSON);
}
bulkRequest.add(request);
}
entry.getValue().clear();
return bulkRequest;
})
.flatMap(bulkRequest ->
Mono.<Boolean>create(sink ->
restClient.getWriteClient()
.bulkAsync(bulkRequest, RequestOptions.DEFAULT, new ActionListener<BulkResponse>() {
@Override
public void onResponse(BulkResponse responses) {
if (responses.hasFailures()) {
sink.error(new RuntimeException("批量存储es数据失败" + responses.buildFailureMessage()));
return;
}
sink.success(!responses.hasFailures());
}
@Override
public void onFailure(Exception e) {
sink.error(e);
}
})))
.then(Mono.just(buffers.size()));
}
private <T> PagerResult<T> translatePageResult(Class<T> clazz, QueryParam param, SearchResponse response) {
long total = response.getHits().getTotalHits();
return PagerResult.of((int) total, translate(clazz, response), param);
}
private <T> List<T> translate(Class<T> clazz, SearchResponse response) {
return Arrays.stream(response.getHits().getHits())
.map(hit -> JSON.toJavaObject(new JSONObject(hit.getSourceAsMap()), clazz))
.collect(Collectors.toList());
}
private Mono<SearchResponse> query(Mono<SearchRequest> requestMono) {
return requestMono.flatMap((request) -> Mono.create(sink -> {
restClient.getQueryClient()
.searchAsync(request, RequestOptions.DEFAULT, translatorActionListener(sink));
}
));
}
private Mono<CountResponse> countQuery(Mono<CountRequest> requestMono) {
return requestMono.flatMap((request) -> Mono.create(sink -> {
restClient.getQueryClient()
.countAsync(request, RequestOptions.DEFAULT, translatorActionListener(sink));
}
));
}
private <T> ActionListener<T> translatorActionListener(MonoSink<T> sink) {
return new ActionListener<T>() {
@Override
public void onResponse(T response) {
sink.success(response);
}
@Override
public void onFailure(Exception e) {
if (e instanceof ElasticsearchException) {
if (((ElasticsearchException) e).status().getStatus() == 404) {
sink.success();
return;
} else if (((ElasticsearchException) e).status().getStatus() == 400) {
sink.error(new ElasticsearchParseException("查询参数格式错误", e));
}
}
sink.error(e);
}
};
}
private Mono<SearchRequest> searchRequestStructure(QueryParam queryParam, ElasticIndex provider) {
return indexOperationService.getIndexMappingMetadata(provider.getStandardIndex())
.switchIfEmpty(Mono.just(IndexMappingMetadata.getInstance(provider.getStandardIndex())))
.map(metadata -> {
SearchRequest request = new SearchRequest(provider.getStandardIndex())
.source(translateService.translate(queryParam, metadata));
if (StringUtils.hasText(provider.getStandardType())) {
request.types(provider.getStandardType());
}
return request;
})
.doOnNext(searchRequest -> log.debug("查询index{},es查询参数:{}", provider.getStandardIndex(), searchRequest.source().toString()));
}
private Mono<CountRequest> countRequestStructure(QueryParam queryParam, ElasticIndex provider) {
queryParam.setPaging(false);
return indexOperationService.getIndexMappingMetadata(provider.getStandardIndex())
.map(metadata -> new CountRequest(provider.getStandardIndex())
.source(translateService.translate(queryParam, metadata)))
.doOnNext(searchRequest -> log.debug("查询index{},es查询参数:{}", provider.getStandardIndex(), searchRequest.source().toString()));
}
}

View File

@ -0,0 +1,24 @@
package org.jetlinks.community.elastic.search.service;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.hswebframework.web.api.crud.entity.PagerResult;
import org.jetlinks.community.elastic.search.index.ElasticIndex;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
public interface ElasticSearchService {
<T> Mono<PagerResult<T>> queryPager(ElasticIndex index, QueryParam queryParam, Class<T> type);
<T> Flux<T> query(ElasticIndex index, QueryParam queryParam, Class<T> type);
Mono<Long> count(ElasticIndex index, QueryParam queryParam);
<T> Mono<Void> commit(ElasticIndex index, T payload);
<T> Mono<Void> commit(ElasticIndex index, Collection<T> payload);
}

View File

@ -0,0 +1,18 @@
package org.jetlinks.community.elastic.search.service;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
import reactor.core.publisher.Mono;
/**
* @author bsetfeng
* @since 1.0
**/
public interface IndexOperationService {
Mono<Boolean> indexIsExists(String index);
Mono<Boolean> init(CreateIndexRequest request);
Mono<IndexMappingMetadata> getIndexMappingMetadata(String index);
}

View File

@ -0,0 +1,47 @@
package org.jetlinks.community.elastic.search.translate;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.jetlinks.community.elastic.search.enums.LinkTypeEnum;
import org.springframework.util.StringUtils;
import java.util.Objects;
/**
* @author bsetfeng
* @since 1.0
**/
@Slf4j
public class QueryParamTranslator {
public static QueryBuilder translate(QueryParam queryParam) {
BoolQueryBuilder query = QueryBuilders.boolQuery();
Objects.requireNonNull(queryParam, "QueryParam must not null.")
.getTerms()
.forEach(term -> LinkTypeEnum.of(term.getType().name())
.ifPresent(e -> e.process(query, term)));
return query;
}
public static SearchSourceBuilder transSourceBuilder(QueryParam queryParam) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
if (queryParam.isPaging()) {
sourceBuilder.from(queryParam.getPageIndex() * queryParam.getPageSize());
sourceBuilder.size(queryParam.getPageSize());
}
queryParam.getSorts()
.forEach(sort -> {
if (!StringUtils.isEmpty(sort.getName())) {
sourceBuilder.sort(sort.getName(), SortOrder.fromString(sort.getOrder()));
}
});
return sourceBuilder.query(translate(queryParam));
}
}

View File

@ -0,0 +1,53 @@
package org.jetlinks.community.elastic.search.utils;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* @author bsetfeng
* @since 1.0
**/
@Slf4j
public class DateTimeUtils {
public static Object formatDateToTimestamp(Object date, List<String> formats) {
return TermCommonUtils.getStandardsTermValue(
formatDateArrayToTimestamp(TermCommonUtils.convertToList(date), formats)
);
}
private static Object formatDateStringToTimestamp(String dateString, List<String> formats) {
for (String format : formats) {
try {
return formatDateStringToTimestamp(dateString, format);
} catch (Exception e) {
log.debug("按格式:{}解析时间字符串:{}错误", format, dateString);
}
}
throw new UnsupportedOperationException("不支持的时间转换" + "formats:" +
JSON.toJSONString(formats) + "dateString:" + dateString);
}
private static long formatDateStringToTimestamp(String dateString, String format) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format);
LocalDateTime dateTime = LocalDateTime.parse(dateString, dateTimeFormatter);
return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
private static List<Object> formatDateArrayToTimestamp(List<Object> values, List<String> formats) {
List<Object> result = new ArrayList<>();
for (Object value : values) {
if (value instanceof String) {
result.add(formatDateStringToTimestamp(value.toString(), formats));
} else {
result.add(value);
}
}
return result;
}
}

View File

@ -0,0 +1,36 @@
package org.jetlinks.community.elastic.search.utils;
import java.util.*;
/**
* @author bsetfeng
* @since 1.0
**/
public class TermCommonUtils {
public static List<Object> convertToList(Object value) {
if (value == null) {
return Collections.emptyList();
}
if (value instanceof String) {
value = ((String) value).split(",");
}
if (value instanceof Object[]) {
value = Arrays.asList(((Object[]) value));
}
if (value instanceof Collection) {
return new ArrayList<Object>(((Collection) value));
}
return Arrays.asList(value);
}
public static Object getStandardsTermValue(List<Object> value) {
if (value.size() > 0 && value.size() < 2) {
return value.get(0);
}
return value;
}
}

View File

@ -0,0 +1,170 @@
package org.jetlinks.community.elastic.search;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.jetlinks.community.elastic.search.aggreation.bucket.Bucket;
import org.jetlinks.community.elastic.search.aggreation.bucket.Sort;
import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
import org.jetlinks.community.elastic.search.aggreation.enums.BucketType;
import org.jetlinks.community.elastic.search.aggreation.enums.MetricsType;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
import org.jetlinks.community.elastic.search.index.DefaultIndexOperationService;
import org.jetlinks.community.elastic.search.index.mapping.IndicesMappingCenter;
import org.jetlinks.community.elastic.search.service.AggregationService;
import org.jetlinks.community.elastic.search.aggreation.DefaultAggregationService;
import org.jetlinks.community.elastic.search.index.ElasticIndex;
import org.jetlinks.community.elastic.search.parser.DefaultLinkTypeParser;
import org.jetlinks.community.elastic.search.parser.DefaultQueryParamTranslateService;
import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
import org.jetlinks.community.elastic.search.service.IndexOperationService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author bsetfeng
* @since 1.0
**/
@Slf4j
public class AggregationTest {
private ElasticRestClient client;
private AggregationService aggregationService;
private QueryParamTranslateService translateService;
private IndexOperationService indexOperationService;
@BeforeEach
public void before() {
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9200, "http")));
client = new ElasticRestClient(restHighLevelClient,restHighLevelClient);
translateService = new DefaultQueryParamTranslateService(new DefaultLinkTypeParser());
indexOperationService = new DefaultIndexOperationService(client, new IndicesMappingCenter());
aggregationService = new DefaultAggregationService(indexOperationService, client, translateService);
}
@Test
@SneakyThrows
public void minTest() {
MetricsAggregationStructure structure = new MetricsAggregationStructure();
structure.setField("lineNumber");
structure.setType(MetricsType.MIN);
aggregationService.metricsAggregation(
QueryParamEntity.of(), structure,
ElasticIndex.createDefaultIndex(() -> "system_log", () -> "doc"))
.doOnNext(metricsResponse -> {
log.info("lineNumber 最小值结果:{}", metricsResponse.getSingleResult().getValueAsString());
})
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
@SneakyThrows
public void termsTest() {
BucketAggregationsStructure structure = new BucketAggregationsStructure();
structure.setField("lineNumber");
structure.setType(BucketType.TERMS);
structure.setSort(Sort.asc());
structure.setSize(2);
aggregationService.bucketAggregation(
QueryParamEntity.of(), structure,
ElasticIndex.createDefaultIndex(() -> "system_log", () -> "doc"))
.doOnNext(bucketResponse -> {
log.info("lineNumber terms聚合结果:{}", JSON.toJSONString(bucketResponse, SerializerFeature.PrettyFormat));
})
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
@SneakyThrows
public void termsNestMetricsTest() {
BucketAggregationsStructure structureTime = new BucketAggregationsStructure();
structureTime.setField("name");
structureTime.setType(BucketType.TERMS);
structureTime.setSubMetricsAggregation(Collections.singletonList(MetricsAggregationStructure.builder()
.field("value")
.type(MetricsType.AVG)
.build()));
BucketAggregationsStructure structure = new BucketAggregationsStructure();
structure.setField("@timestamp");
structure.setType(BucketType.DATE_HISTOGRAM);
structure.setFormat("yyyy-MM-dd");
structure.setInterval("1d");
structure.setSort(Sort.asc());
structure.setSubBucketAggregation(Collections.singletonList(structureTime));
aggregationService.bucketAggregation(
QueryParamEntity.of("id.keyword", "Metaspace"), structure,
ElasticIndex.createDefaultIndex(() -> "metrics-2019-12", () -> ""))
.doOnNext(bucketResponse -> {
bucketResponse.getBuckets()
.forEach(bucket -> {
Map<String, Double> map = bucket.getBuckets()
.stream()
.collect(Collectors.toMap(Bucket::getKey, b-> b.getAvg().getValue()));
Double committed = map.get("jvm_memory_committed");
Double used = map.get("jvm_memory_used");
if (committed != null && used != null) {
double result = committed / (used + committed);
bucket.setAvg(MetricsResponseSingleValue.builder()
.value(result)
.valueAsString(String.valueOf(result))
.build());
} else {
bucket.setAvg(MetricsResponseSingleValue.builder()
.value(0)
.valueAsString("0")
.build());
log.error("获取jvm内存使用率异常, jvm可用内存:{}, jvm已使用内存:{},key:{}", committed, used, bucket.getKey());
}
});
log.info("lineNumber terms聚合结果:{}", JSON.toJSONString(bucketResponse, SerializerFeature.PrettyFormat));
})
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
@SneakyThrows
public void termsNestBucketTest() {
BucketAggregationsStructure structure = new BucketAggregationsStructure();
structure.setField("lineNumber");
structure.setType(BucketType.TERMS);
structure.setSort(Sort.asc());
structure.setSubBucketAggregation(Arrays.asList(BucketAggregationsStructure.builder()
.field("createTime")
.type(BucketType.TERMS)
.build()));
aggregationService.bucketAggregation(
QueryParamEntity.of(), structure,
ElasticIndex.createDefaultIndex(() -> "system_log", () -> "doc"))
.doOnNext(bucketResponse -> {
log.info("lineNumber terms聚合结果:{}", JSON.toJSONString(bucketResponse, SerializerFeature.PrettyFormat));
})
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}

View File

@ -0,0 +1,47 @@
package org.jetlinks.community.elastic.search;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.hswebframework.ezorm.core.dsl.Query;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
import org.jetlinks.community.elastic.search.parser.DefaultLinkTypeParser;
import org.jetlinks.community.elastic.search.parser.DefaultQueryParamTranslateService;
import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
/**
* @author bsetfeng
* @since 1.0
**/
public class ElasticSearchQueryParamTranslatorTest {
@Test
public void test() {
Query query = Query.of(new QueryParam())
.where()
.is("methodName", "error")
.or()
.in("level", "ERROR", "DEBUG")
.orNest()
.or()
.is("threadId", "44")
.or()
.is("lineNumber", "319")
.between("aaa", "2019-12-11 22:00:10", "2019-12-11 23:00:10")
.end();
QueryParamTranslateService translateService = new DefaultQueryParamTranslateService(new DefaultLinkTypeParser());
SearchSourceBuilder searchSourceBuilder = translateService.translate(query.getParam(), IndexMappingMetadata.getInstance(""));
JSONObject jsonObject = JSON.parseObject(searchSourceBuilder.query().toString());
JSONObject boolJson = jsonObject.getJSONObject("bool");
Assert.assertEquals(boolJson.getJSONArray("must").size(), 1);
Assert.assertEquals(boolJson.getJSONArray("should").size(), 2);
System.out.println(searchSourceBuilder.query().toString());
}
}

View File

@ -0,0 +1,61 @@
package org.jetlinks.community.elastic.search;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.jetlinks.community.elastic.search.enums.FieldDateFormat;
import org.jetlinks.community.elastic.search.enums.FieldType;
import org.jetlinks.community.elastic.search.index.CreateIndex;
import org.jetlinks.community.elastic.search.index.DefaultIndexOperationService;
import org.jetlinks.community.elastic.search.index.mapping.IndicesMappingCenter;
import org.jetlinks.community.elastic.search.service.IndexOperationService;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
/**
* @author bsetfeng
* @since 1.0
**/
@Slf4j
public class IndexInitTest {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9200, "http")));
IndexOperationService operationService =
new DefaultIndexOperationService(new ElasticRestClient(client,client),new IndicesMappingCenter());
@Test
@SneakyThrows
public void simpleTest() {
CreateIndexRequest request = CreateIndex.createInstance()
.addIndex("bestfeng")
.createMapping()
.addFieldName("date").addFieldType(FieldType.DATE).addFieldDateFormat(FieldDateFormat.epoch_millis, FieldDateFormat.simple_date).commit()
.addFieldName("name").addFieldType(FieldType.KEYWORD).commit()
.end()
.createSettings()
.settingReplicas(2)
.settingShards(6)
.end()
.createIndexRequest();
operationService.init(request)
.as(StepVerifier::create)
.expectNextMatches(bool -> bool)
.verifyComplete();
}
public static void main(String[] args) {
Date date2 = new Date(1575528676826L);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime2 = LocalDateTime.ofInstant(date2.toInstant(), ZoneId.systemDefault());
System.out.println(localDateTime2.format(formatter));
}
}

View File

@ -0,0 +1,3 @@
# 网关模块
统一管理设备网关服务,消息网关.

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jetlinks-components</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway-component</artifactId>
<dependencies>
<dependency>
<groupId>org.jetlinks</groupId>
<artifactId>jetlinks-core</artifactId>
<version>${jetlinks.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>network-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,62 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 客户端连接会话,用于保存客户端订阅信息.
*
* @author zhouhao
* @version 1.0
* @since 1.0
*/
public interface ClientSession {
/**
* @return 会话ID
*/
String getId();
/**
* @return 客户端ID
*/
String getClientId();
/**
* @return 是否持久化
*/
boolean isPersist();
/**
* 获取会话的全部订阅信息
*
* @return 订阅信息
*/
Flux<Subscription> getSubscriptions();
/**
* 添加订阅信息
*
* @param subscription 订阅信息
* @return 添加结果
*/
Mono<Void> addSubscription(Subscription subscription);
/**
* 移除订阅信息
*
* @param subscription 订阅信息
* @return 移除结果
*/
Mono<Void> removeSubscription(Subscription subscription);
/**
* @return 会话是否存活
*/
boolean isAlive();
/**
* 关闭会话
*/
void close();
}

View File

@ -0,0 +1,61 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
/**
* 客户端会话管理器,用于管理客户端会话.
*
* @author zhouhao
* @version 1.0
* @since 1.0
*/
public interface ClientSessionManager {
/**
* 创建一个新会话
*
* @param messageGatewayId 消息网关ID
* @param connection 网关消息连接
* @return 创建结果
*/
Mono<ClientSession> createSession(String messageGatewayId, MessageConnection connection);
/**
* 获取网关内全部会话
*
* @param messageGatewayId 网关
* @return 会话流
*/
Flux<ClientSession> getSessions(String messageGatewayId);
/**
* 从指定的网关里获取指定的会话,如果会话不存在则返回{@link Mono#empty()}
*
* @param messageGatewayId 消息网关ID
* @param sessionId 会话ID
* @return 会话
*/
Mono<ClientSession> getSession(String messageGatewayId, String sessionId);
/**
* 批量获取指定网关下的会话
*
* @param messageGatewayId 网关ID
* @param sessionIdList 会话ID
* @return 会话流
*/
Flux<ClientSession> getSessions(String messageGatewayId, Collection<String> sessionIdList);
/**
* 关闭指定网关的会话
*
* @param messageGatewayId 消息网关ID
* @param sessionId 会话ID
* @return 关闭结果
*/
Mono<Void> closeSession(String messageGatewayId, String sessionId);
}

View File

@ -0,0 +1,21 @@
package org.jetlinks.community.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.core.message.codec.EncodedMessage;
import org.jetlinks.core.message.codec.Transport;
@Getter
@Setter
@AllArgsConstructor
class DefaultTopicMessage implements TopicMessage {
private String topic;
private EncodedMessage message;
public DefaultTopicMessage(String topic,Object message){
this.topic=topic;
}
}

View File

@ -0,0 +1,66 @@
package org.jetlinks.community.gateway;
import org.jetlinks.core.message.Message;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.community.network.NetworkType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 设备网关,用于统一管理设备连接,状态以及消息收发
*
* @author zhouhao
* @version 1.0
* @since 1.0
*/
public interface DeviceGateway {
/**
* @return 网关ID
*/
String getId();
/**
* @return 传输协议
* @see org.jetlinks.core.message.codec.DefaultTransport
*/
Transport getTransport();
/**
* @return 网络类型
* @see org.jetlinks.community.network.DefaultNetworkType
*/
NetworkType getNetworkType();
/**
* 订阅来自设备到消息,关闭网关时不会结束流.
*
* @return 设备消息流
*/
Flux<Message> onMessage();
/**
* 启动网关
*
* @return 启动结果
*/
Mono<Void> startup();
/**
* 暂停网关,暂停后停止处理设备消息.
*
* @return 暂停结果
*/
Mono<Void> pause();
/**
* 关闭网关
*
* @return 关闭结果
*/
Mono<Void> shutdown();
default boolean isAlive(){
return true;
}
}

View File

@ -0,0 +1,15 @@
package org.jetlinks.community.gateway;
import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
import reactor.core.publisher.Mono;
import java.util.List;
public interface DeviceGatewayManager {
Mono<DeviceGateway> getGateway(String id);
Mono<Void> shutdown(String gatewayId);
List<DeviceGatewayProvider> getProviders();
}

View File

@ -0,0 +1,12 @@
package org.jetlinks.community.gateway;
import org.jetlinks.core.message.codec.EncodedMessage;
public interface EncodableMessage extends EncodedMessage {
Object getNativePayload();
static EncodableMessage of(Object object) {
return new JsonEncodedMessage(object);
}
}

View File

@ -0,0 +1,33 @@
package org.jetlinks.community.gateway;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import org.jetlinks.core.message.codec.EncodedMessage;
import org.jetlinks.rule.engine.executor.PayloadType;
import javax.annotation.Nonnull;
import java.util.Objects;
public class JsonEncodedMessage implements EncodableMessage {
private volatile ByteBuf payload;
@Getter
private Object nativePayload;
public JsonEncodedMessage(Object nativePayload) {
Objects.requireNonNull(nativePayload);
this.nativePayload = nativePayload;
}
@Nonnull
@Override
public ByteBuf getPayload() {
if (payload == null) {
payload = PayloadType.JSON.write(nativePayload);
}
return payload;
}
}

View File

@ -0,0 +1,80 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Mono;
/**
* 消息连接,在网络组件中,一个客户端则可能认为是一个消息连接.
* <p>
* 如果实现了{@link MessagePublisher}接口,则认为可以从此连接中订阅消息,进行网关转发.
* <p>
* 如果实现了{@link MessageSubscriber}接口,则认为连接可以进行消息订阅,并接收来自网关的消息.
*
* @see MessagePublisher
* @see MessageSubscriber
*/
public interface MessageConnection {
/**
* @return 连接唯一标识
*/
String getId();
/**
* 添加断开连接监听器,当网络断开时,执行{@link Runnable#run()},支持多个监听器
*
* @param disconnectListener 监听器
*/
void onDisconnect(Runnable disconnectListener);
/**
* 主动断开连接
*/
void disconnect();
/**
* 当前连接是否存活
*
* @return 是否存活
*/
boolean isAlive();
/**
* 判断是否为推送器,如果是则可以使用{@link this#asPublisher()}转为推送器,然后进行消息订阅等处理.
*
* @return 是否为推送器
* @see MessagePublisher
* @see this#asPublisher()
*/
default boolean isPublisher() {
return this instanceof MessagePublisher;
}
/**
* {@link this#isPublisher()}
*
* @return 是否为订阅器
*/
default boolean isSubscriber() {
return this instanceof MessageSubscriber;
}
/**
* 尝试转为消息推送器,如果不可行则返回{@link Mono#empty()}
*
* @return MessagePublisher
* @see MessagePublisher
*/
default Mono<MessagePublisher> asPublisher() {
return isPublisher() ? Mono.just(this).cast(MessagePublisher.class) : Mono.empty();
}
/**
* 尝试转为消息订阅,如果不可行则返回{@link Mono#empty()}
*
* @return MessageSubscriber
* @see MessageSubscriber
*/
default Mono<MessageSubscriber> asSubscriber() {
return isSubscriber() ? Mono.just(this).cast(MessageSubscriber.class) : Mono.empty();
}
}

View File

@ -0,0 +1,57 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Flux;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* 网关消息连接器,用于连接自定义到消息到网关.
* <p>
* 如果连接器没有任何连接订阅者,将拒绝连接{@link MessageConnection#disconnect()}.
* <p>
* ️注意: 连接器不会保存任何连接信息
*
* @author zhouhao
* @see MessageConnection
*/
public interface MessageConnector {
/**
* @return 连接器唯一标识
*/
@Nonnull
String getId();
/**
* @return 名称
*/
@Nullable
default String getName() {
return getId();
}
/**
* @return 说明
*/
@Nullable
default String getDescription() {
return null;
}
/**
* 订阅连接器中到网络连接,此订阅只会获取到最新的连接.
* <p>
* 如果订阅了多次,每个订阅都会收到每一个连接.
* <p>
* : 如果发生错误不想停止订阅,请处理好{@link Flux#onErrorContinue(BiConsumer)}或者{@link Flux#onErrorResume(Function)}
*
* @return 网络连接流.
* @see MessageConnection
*/
@Nonnull
Flux<MessageConnection> onConnection();
}

View File

@ -0,0 +1,103 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Flux;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 基于topic对消息网关,用于对各种消息进行路由和转发.
* <p>
* : 使用 CoAP 发送 /group/1/user/1 消息到网关,其他任何订阅了此topic的客户端将都收到此消息.
*
* @author zhouhao
* @see 1.0
* @see MessageConnector
*/
public interface MessageGateway {
/**
* @return 网关ID
*/
String getId();
String getName();
/**
* 向网关推送消息,消息将根据{@link TopicMessage#getTopic()}发送到对应的订阅者.
*
* @param message 消息
* @param shareCluster 是否广播到集群其他节点
* @return 成功推送的会话流
*/
Flux<ClientSession> publish(TopicMessage message, boolean shareCluster);
/**
* 向网关推送消息.
*
* @param topic 话题
* @param payload 消息内容
* @param shareCluster 是否广播到集群其他节点
* @return 成功推送的会话流
* @see this#publish(TopicMessage, boolean)
*/
default Flux<ClientSession> publish(String topic, Object payload, boolean shareCluster) {
return publish(TopicMessage.of(topic, payload), shareCluster);
}
default Flux<ClientSession> publish(String topic, Object payload) {
return publish(topic, payload, false);
}
default Flux<ClientSession> publish(TopicMessage message) {
return publish(message, false);
}
/**
* 订阅当前网关收到的消息.
*
* @param subscription 订阅信息
* @param shareCluster 是否共享集群中其他节点到消息
* @return 信息流
*/
Flux<TopicMessage> subscribe(Collection<Subscription> subscription, boolean shareCluster);
/**
* 订阅当前网关收到的消息.
* 如果存在集群,不会收到来自集群其他节点的消息.
*
* @param topics 话题数组,可同时订阅多个话题
* @return 消息
*/
default Flux<TopicMessage> subscribe(String... topics) {
return subscribe(Stream.of(topics).map(Subscription::new).collect(Collectors.toList()), false);
}
/**
* 注册一个消息连接器,用于进行真实的消息收发
*
* @param connector 连接器
*/
void registerMessageConnector(MessageConnector connector);
/**
* 根据连接器ID删除一个连接器
*
* @param connectorId 连接器ID {@link MessageConnector#getId()}
* @return 被删除的连接器, 连接器不存在则返回<code>null</code>
* @see MessageConnector
*/
MessageConnector removeConnector(String connectorId);
/**
* 启动网关
*/
void startup();
/**
* 停止网关
*/
void shutdown();
}

View File

@ -0,0 +1,12 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface MessageGatewayManager {
Mono<MessageGateway> getGateway(String id);
Flux<MessageGateway> getAllGateway();
}

View File

@ -0,0 +1,24 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Flux;
import javax.annotation.Nonnull;
/**
* 消息推送器,可通过{@link this#onMessage()}来订阅推送器中到消息
*
* @see MessageConnection
* @since 1.0
*/
public interface MessagePublisher {
/**
* 订阅连接中的消息
*
* @return 消息流
*/
@Nonnull
Flux<TopicMessage> onMessage();
}

View File

@ -0,0 +1,56 @@
package org.jetlinks.community.gateway;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
/**
* 消息订阅器,订阅器用于订阅来自其他连接器的消息.
*
* @author zhouhao
* @see MessageConnection
* @see MessagePublisher
* @since 1.0
*/
public interface MessageSubscriber {
/**
* 推送消息到订阅器,处理消息应该是无阻塞的,并且快速失败.
*
* @param message 消息
* @return 处理结果
*/
@Nonnull
Mono<Void> publish(@Nonnull TopicMessage message);
/**
* 监听订阅请求,网关收到订阅请求后才会将对应的广播消息推送{@link this#publish(TopicMessage)}到该订阅器.
*
* @return 订阅流
*/
@Nonnull
Flux<Subscription> onSubscribe();
/**
* 监听取消订阅,取消订阅后,将不会再收到该话题的消息
*
* @return 取消订阅流
*/
@Nonnull
Flux<Subscription> onUnSubscribe();
/**
* 是否共享集群中的消息
* <p>
* 如果为<code>true</code>,
* 则当集群当其他节点收到消息时,也会调用{@link this#publish(TopicMessage)}.
* : 如果同一个订阅者在集群中多个节点进行相同的订阅,则会收到相同的消息. 在一些场景下(比如业务系统消息队列)不建议设置为true.
* <p>
* 如果为<code>false</code>
* 则只会收到当前服务器的消息
*
* @return 是否共享集群中的消息
*/
boolean isShareCluster();
}

View File

@ -0,0 +1,26 @@
package org.jetlinks.community.gateway;
import lombok.*;
/**
* 订阅信息.支持通配符**(匹配多层目录)*(匹配单层目录).
*
* @author zhouhao
* @since 1.0
*/
@Getter
@Setter
@EqualsAndHashCode(of = "topic")
public class Subscription {
private String topic;
public Subscription(String topic) {
//适配mqtt topic通配符
if (topic.contains("#") || topic.contains("+")) {
topic = topic.replace("#", "**").replace("+", "*");
}
this.topic = topic;
}
}

View File

@ -0,0 +1,39 @@
package org.jetlinks.community.gateway;
import org.jetlinks.core.message.codec.EncodedMessage;
import javax.annotation.Nonnull;
public interface TopicMessage {
/**
* 主题: 格式为: /group/1/user/1, 支持通配符: **(多层路径),*(单层路径)
*
* <pre>
* /group/** , /group/下的全部topic.包括子目录
* /group/1/* , /group/1/下的topic. 不包括子目录
* </pre>
*
* @return topic
*/
@Nonnull
String getTopic();
/**
* @return 已编码的消息
* @see org.jetlinks.core.message.codec.MqttMessage
*/
@Nonnull
EncodedMessage getMessage();
static TopicMessage of(String topic, EncodedMessage message) {
return new DefaultTopicMessage(topic, message);
}
static TopicMessage of(String topic, Object payload) {
if (payload instanceof EncodedMessage) {
return of(topic, ((EncodedMessage) payload));
}
return of(topic, EncodableMessage.of(payload));
}
}

View File

@ -0,0 +1,182 @@
package org.jetlinks.community.gateway;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiFunction;
@Getter
@Setter
@EqualsAndHashCode(of = "part")
public class TopicPart {
private TopicPart parent;
private String part;
private volatile String topic;
private int depth;
private ConcurrentMap<String, TopicPart> child = new ConcurrentHashMap<>();
private Set<String> sessionId = new CopyOnWriteArraySet<>();
private static final AntPathMatcher matcher = new AntPathMatcher();
public TopicPart(TopicPart parent, String part) {
if (StringUtils.isEmpty(part) || part.equals("/")) {
this.part = "";
} else {
if (part.contains("/")) {
this.ofTopic(part);
} else {
this.part = part;
}
}
this.parent = parent;
if (null != parent) {
this.depth = parent.depth + 1;
}
}
public String getTopic() {
if (topic == null) {
TopicPart parent = getParent();
StringBuilder builder = new StringBuilder();
if (parent != null) {
String parentTopic = parent.getTopic();
builder.append(parentTopic).append(parentTopic.equals("/") ? "" : "/");
} else {
builder.append("/");
}
return topic = builder.append(part).toString();
}
return topic;
}
public TopicPart subscribe(String topic) {
return getOrDefault(topic, TopicPart::new);
}
public void addSessionId(String... sessionId) {
this.sessionId.addAll(Arrays.asList(sessionId));
}
public void removeSession(String... sessionId) {
this.sessionId.removeAll(Arrays.asList(sessionId));
}
private void ofTopic(String topic) {
String[] parts = topic.split("[/]", 2);
this.part = parts[0];
if (parts.length > 1) {
TopicPart part = new TopicPart(this, parts[1]);
this.child.put(part.part, part);
}
}
private TopicPart getOrDefault(String topic, BiFunction<TopicPart, String, TopicPart> mapping) {
if (topic.startsWith("/")) {
topic = topic.substring(1);
}
String[] parts = topic.split("[/]");
TopicPart part = child.computeIfAbsent(parts[0], _topic -> mapping.apply(this, _topic));
for (int i = 1; i < parts.length && part != null; i++) {
TopicPart parent = part;
part = part.child.computeIfAbsent(parts[i], _topic -> mapping.apply(parent, _topic));
}
return part;
}
public Mono<TopicPart> get(String topic) {
return Mono.justOrEmpty(getOrDefault(topic, ((topicPart, s) -> null)));
}
public Flux<TopicPart> find(String topic) {
return find(topic, this);
}
@Override
public String toString() {
return "topic: " + getTopic() + ", sessions: " + sessionId.size();
}
public static Flux<TopicPart> find(String topic,
TopicPart topicPart) {
return Flux.create(sink -> {
ArrayDeque<TopicPart> cache = new ArrayDeque<>();
cache.add(topicPart);
String[] topicParts = topic.split("[/]");
String nextPart = null;
while (!cache.isEmpty() && !sink.isCancelled()) {
TopicPart part = cache.poll();
if (part == null) {
break;
}
if (part.part.equals("**")
// || part.part.equals("*")
|| matcher.match(part.getTopic(), topic)
|| (matcher.match(topic, part.getTopic()))) {
sink.next(part);
}
//订阅了如 /device/**/event/*
if (part.part.equals("**")) {
TopicPart tmp = null;
for (int i = part.depth; i < topicParts.length; i++) {
tmp = part.child.get(topicParts[i]);
if (tmp != null) {
cache.add(tmp);
}
}
if (null != tmp) {
continue;
}
}
if ("**".equals(nextPart) || "*".equals(nextPart)) {
cache.addAll(part.child.values());
continue;
}
TopicPart next = part.child.get("**");
if (next != null) {
cache.add(next);
}
next = part.child.get("*");
if (next != null) {
cache.add(next);
}
if (part.depth + 1 >= topicParts.length) {
continue;
}
nextPart = topicParts[part.depth + 1];
if (nextPart.equals("*") || nextPart.equals("**")) {
cache.addAll(part.child.values());
continue;
}
next = part.child.get(nextPart);
if (next != null) {
cache.add(next);
}
}
sink.complete();
});
}
}

View File

@ -0,0 +1,68 @@
package org.jetlinks.community.gateway.rule;
import org.hswebframework.web.exception.NotFoundException;
import org.jetlinks.community.gateway.MessageGatewayManager;
import org.jetlinks.community.network.PubSubType;
import org.jetlinks.rule.engine.api.RuleData;
import org.jetlinks.rule.engine.api.executor.ExecutionContext;
import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
import org.reactivestreams.Publisher;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.function.Function;
@Component
public class MessageGatewayRuleNode extends CommonExecutableRuleNodeFactoryStrategy<MessageGatewayRuleNodeConfig> {
private final MessageGatewayManager gatewayManager;
static {
TopicMessageCodec.register();
}
public MessageGatewayRuleNode(MessageGatewayManager gatewayManager) {
this.gatewayManager = gatewayManager;
}
@Override
public Function<RuleData, ? extends Publisher<?>> createExecutor(ExecutionContext context, MessageGatewayRuleNodeConfig config) {
if (config.getType() == PubSubType.consumer) {
return Mono::just;
}
return ruleData -> gatewayManager
.getGateway(config.getGatewayId())
.switchIfEmpty(Mono.error(() -> new NotFoundException("消息网关[{" + config.getGatewayId() + "}]不存在")))
.flatMap(gateway -> config.convert(ruleData)
.flatMap(msg -> gateway.publish(msg, config.isShareCluster()))
.then())
.thenReturn(ruleData);
}
@Override
protected void onStarted(ExecutionContext context, MessageGatewayRuleNodeConfig config) {
super.onStarted(context, config);
if (config.getType() == PubSubType.producer) {
return;
}
//订阅网关中的消息
context.onStop(gatewayManager
.getGateway(config.getGatewayId())
.switchIfEmpty(Mono.fromRunnable(() -> context.logger().error("消息网关[{" + config.getGatewayId() + "}]不存在")))
.flatMapMany(gateway -> gateway.subscribe(config.createTopics()))
.map(config::convert)
.flatMap(data -> context.getOutput().write(Mono.just(RuleData.create(data))))
.onErrorContinue((err, obj) -> {
context.logger().error(err.getMessage(), err);
})
.subscribe()::dispose);
}
@Override
public String getSupportType() {
return "message-gateway";
}
}

View File

@ -0,0 +1,56 @@
package org.jetlinks.community.gateway.rule;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.community.gateway.TopicMessage;
import org.jetlinks.community.network.PubSubType;
import org.jetlinks.rule.engine.api.RuleData;
import org.jetlinks.rule.engine.api.model.NodeType;
import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
@Getter
@Setter
public class MessageGatewayRuleNodeConfig implements RuleNodeConfig {
private String gatewayId;
private PubSubType type;
private String topics;
private boolean shareCluster;
public Flux<TopicMessage> convert(RuleData data) {
return TopicMessageCodec.getInstance()
.decode(data, TopicMessageCodec.feature(createTopics()));
}
public Object convert(TopicMessage message) {
return TopicMessageCodec.getInstance().encode(message);
}
public String[] createTopics() {
return topics.split("[,;\n]");
}
@Override
public void validate() {
Assert.hasText(gatewayId, "gatewayId can not be empty");
Assert.hasText(topics, "topics can not be empty");
Assert.notNull(type, "type can not be null");
}
@Override
public NodeType getNodeType() {
return NodeType.MAP;
}
@Override
public void setNodeType(NodeType nodeType) {
}
}

View File

@ -0,0 +1,73 @@
package org.jetlinks.community.gateway.rule;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.community.gateway.TopicMessage;
import org.jetlinks.rule.engine.api.RuleData;
import org.jetlinks.rule.engine.api.RuleDataCodec;
import org.jetlinks.rule.engine.api.RuleDataCodecs;
import org.jetlinks.rule.engine.executor.PayloadType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
public class TopicMessageCodec implements RuleDataCodec<TopicMessage> {
private static final TopicMessageCodec INSTANCE = new TopicMessageCodec();
static {
RuleDataCodecs.register(TopicMessage.class, INSTANCE);
}
public static void register() {
}
public static TopicMessageCodec getInstance() {
return INSTANCE;
}
@Override
public Map<String, Object> encode(TopicMessage data, Feature... features) {
ByteBuf payload = data.getMessage().getPayload();
PayloadType payloadType = PayloadType.valueOf(data.getMessage().getPayloadType().name());
Map<String, Object> map = new HashMap<>();
map.put("topic", data.getTopic());
map.put("message", payloadType.read(payload));
return map;
}
@Override
public Flux<TopicMessage> decode(RuleData data, Feature... features) {
return Mono.fromSupplier(() -> Feature.find(TopicFeature.class, features)
.map(TopicFeature::getTopics)
.orElseThrow(() -> new UnsupportedOperationException("topics not found")))
.flatMapMany(Flux::just)
.flatMap(topic -> data
.dataToMap()
.map(map -> TopicMessage.of(topic, map)));
}
public static TopicFeature feature(String... topics) {
return new TopicFeature(topics);
}
@Getter
@Setter
public static class TopicFeature implements RuleDataCodec.Feature {
private String[] topics;
public TopicFeature(String... topics) {
this.topics = topics;
}
}
}

View File

@ -0,0 +1,79 @@
package org.jetlinks.community.gateway.supports;
import org.jetlinks.community.gateway.DeviceGateway;
import org.jetlinks.community.gateway.DeviceGatewayManager;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class DefaultDeviceGatewayManager implements DeviceGatewayManager, BeanPostProcessor {
private final DeviceGatewayPropertiesManager propertiesManager;
private Map<String, DeviceGatewayProvider> providers = new ConcurrentHashMap<>();
private Map<String, DeviceGateway> store = new ConcurrentHashMap<>();
public DefaultDeviceGatewayManager(DeviceGatewayPropertiesManager propertiesManager) {
this.propertiesManager = propertiesManager;
}
private Mono<DeviceGateway> doGetGateway(String id) {
if (store.containsKey(id)) {
return Mono.just(store.get(id));
}
return propertiesManager
.getProperties(id)
.switchIfEmpty(Mono.error(new UnsupportedOperationException("网关配置[" + id + "]不存在")))
.flatMap(properties -> Mono
.justOrEmpty(providers.get(properties.getProvider()))
.switchIfEmpty(Mono.error(new UnsupportedOperationException("不支持的网络服务[" + properties.getProvider() + "]")))
.flatMap(provider -> provider
.createDeviceGateway(properties)
.flatMap(gateway -> {
if (store.containsKey(id)) {
return gateway
.shutdown()
.thenReturn(store.get(id));
}
store.put(id, gateway);
return Mono.justOrEmpty(gateway);
})));
}
@Override
public Mono<Void> shutdown(String gatewayId) {
return Mono.justOrEmpty(store.remove(gatewayId))
.flatMap(DeviceGateway::shutdown);
}
@Override
public Mono<DeviceGateway> getGateway(String id) {
return Mono
.justOrEmpty(store.get(id))
.switchIfEmpty(doGetGateway(id));
}
@Override
public List<DeviceGatewayProvider> getProviders() {
return new ArrayList<>(providers.values());
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DeviceGatewayProvider) {
DeviceGatewayProvider provider = ((DeviceGatewayProvider) bean);
providers.put(provider.getId(), provider);
}
return bean;
}
}

View File

@ -0,0 +1,256 @@
package org.jetlinks.community.gateway.supports;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.id.IDGenerator;
import org.jetlinks.community.gateway.*;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
@Slf4j
public class DefaultMessageGateway implements MessageGateway {
@Getter
private final String id;
@Getter
private String name;
private TopicPart root = new TopicPart(null, "/");
private Map<String, ConnectionSession> sessions = new ConcurrentHashMap<>();
private ClientSessionManager sessionManager;
private Map<String, Connector> connectors = new ConcurrentHashMap<>();
private AtomicBoolean started = new AtomicBoolean();
private LocalMessageConnector localGatewayConnector;
public DefaultMessageGateway(String id, ClientSessionManager sessionManager) {
this(id, id, sessionManager);
}
public DefaultMessageGateway(String id, String name, ClientSessionManager sessionManager) {
this.id = id;
this.name = name;
this.sessionManager = sessionManager;
this.localGatewayConnector = new LocalMessageConnector();
this.registerMessageConnector(localGatewayConnector);
}
@Override
public Flux<ClientSession> publish(TopicMessage message, boolean shareCluster) {
return publishLocal(message, session -> true);
}
@Override
public Flux<TopicMessage> subscribe(Collection<Subscription> subscriptions, boolean shareCluster) {
return Flux.defer(() -> {
LocalMessageConnection networkConnection = localGatewayConnector.addConnection("local:" + IDGenerator.SNOW_FLAKE_STRING.generate(), shareCluster);
return networkConnection
.onLocalMessage()
.doOnSubscribe(sub -> subscriptions.forEach(networkConnection::addSubscription))
.doFinally((s) -> networkConnection.disconnect());
});
}
@Override
public void registerMessageConnector(MessageConnector connector) {
if (null != removeConnector(connector.getId())) {
log.warn("connector exists , shutdown it !");
}
Connector _connector = new Connector(connector);
connectors.put(connector.getId(), _connector);
if (started.get()) {
_connector.startup();
}
}
@Override
public MessageConnector removeConnector(String connectorId) {
Connector connector = connectors.remove(connectorId);
if (connector != null) {
connector.shutdown();
return connector.connector;
}
return null;
}
private Flux<ClientSession> publishLocal(TopicMessage message,
Predicate<ConnectionSession> filter) {
return Flux.defer(() -> root.find(message.getTopic())
.flatMapIterable(TopicPart::getSessionId)
.flatMap(id -> Mono.justOrEmpty(sessions.get(id)))
.filter(connectionSession -> connectionSession.isAlive() && filter.test(connectionSession))
.flatMap(session ->
session.connection
.asSubscriber()
.flatMap(subscriber -> subscriber.publish(message))
.doOnSuccess(nil -> {
log.debug("publish message [{}] to session:[{}] complete", message.getTopic(), session.getId());
})
.onErrorContinue((err, se) -> {
log.error("publish message [{}] to session:[{}] error", message.getTopic(), session.getId(), err);
})
.thenReturn(session))
.map(ConnectionSession::getSession))
;
}
@Override
public void startup() {
Flux.interval(Duration.ofSeconds(10))
.subscribe();
if (!started.getAndSet(true)) {
for (Connector value : connectors.values()) {
if (value.disposable == null) {
value.startup();
}
}
}
}
@Override
public void shutdown() {
started.set(false);
for (Connector value : connectors.values()) {
value.shutdown();
}
}
private Mono<Void> dispatch(ConnectionSession from, TopicMessage message) {
//转发到其他topic
return publishLocal(message, session -> true).then();
}
@Getter
class ConnectionSession {
String id;
Connector connector;
ClientSession session;
MessageConnection connection;
Disposable disposable;
boolean onlyConsumeLocal;
boolean isAlive() {
return connection.isAlive();
}
void init() {
disposable = connection
.asSubscriber()
.subscribe(subscriber -> {
subscriber
.onSubscribe()
.takeWhile(r -> isAlive())
.flatMap(subscription -> {
if (log.isDebugEnabled()) {
log.debug("session:[{}] subscribe:[{}]", session.getId(), subscription.getTopic());
}
root.subscribe(subscription.getTopic())
.addSessionId(getId());
return session.addSubscription(subscription)
.thenReturn(subscription);
}).subscribe();
subscriber.onUnSubscribe()
.takeWhile(r -> isAlive())
.flatMap(subscription ->
root.get(subscription.getTopic())
.doOnNext(part -> {
if (log.isDebugEnabled()) {
log.debug("session:[{}] unsubscribe:[{}]", session.getId(), part.getTopic());
}
part.removeSession(getId());
})
.then(session.removeSubscription(subscription)))
.subscribe();
});
//加载会话已有的订阅信息
session.getSubscriptions()
.map(Subscription::getTopic)
.flatMap(topic -> root.find(topic))
.subscribe(part -> part.addSessionId(getId()));
}
void close() {
if (disposable != null && !disposable.isDisposed()) {
disposable.dispose();
}
sessions.remove(getId());
//取消订阅
session.getSubscriptions()
.map(Subscription::getTopic)
.flatMap(topic -> root.get(topic))
.doOnNext(part -> part.removeSession(getId()))
.then(sessionManager.closeSession(DefaultMessageGateway.this.getId(), getId()))
.doFinally(s -> log.debug("session [{}] closed", getId()))
.subscribe();
}
}
class Connector {
private MessageConnector connector;
private Disposable disposable;
public Connector(MessageConnector connector) {
this.connector = connector;
}
private void shutdown() {
if (disposable != null && !disposable.isDisposed()) {
disposable.dispose();
disposable = null;
}
}
public void startup() {
shutdown();
disposable = connector
.onConnection()
.flatMap(connection -> sessionManager
.createSession(getId(), connection)
.map(session -> {
ConnectionSession connectionSession = new ConnectionSession();
connectionSession.connection = connection;
connectionSession.onlyConsumeLocal = connector instanceof LocalMessageConnector;
connectionSession.session = session;
connectionSession.connector = this;
connectionSession.id = session.getId();
return connectionSession;
}))
.filter(ConnectionSession::isAlive)
.doOnNext(session -> {
sessions.put(session.getId(), session);
session.init();
session.connection.onDisconnect(session::close);
session.getConnection()
.asPublisher()
.flatMapMany(MessagePublisher::onMessage)
.takeWhile(r -> disposable != null)
.flatMap(msg -> dispatch(session, msg))
.onErrorContinue((err, obj) -> {
log.error(err.getMessage(), err);
})
.subscribe();
})
.subscribe();
}
}
}

View File

@ -0,0 +1,40 @@
package org.jetlinks.community.gateway.supports;
import org.jetlinks.community.gateway.MessageGateway;
import org.jetlinks.community.gateway.MessageGatewayManager;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class DefaultMessageGatewayManager implements MessageGatewayManager, BeanPostProcessor {
private Map<String, MessageGateway> cache = new ConcurrentHashMap<>();
@Override
public Mono<MessageGateway> getGateway(String id) {
return Mono.justOrEmpty(cache.get(id));
}
@Override
public Flux<MessageGateway> getAllGateway() {
return Flux.fromIterable(cache.values());
}
public void register(MessageGateway gateway) {
cache.put(gateway.getId(), gateway);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof MessageGateway) {
register(((MessageGateway) bean));
}
return bean;
}
}

View File

@ -0,0 +1,22 @@
package org.jetlinks.community.gateway.supports;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Getter
@Setter
public class DeviceGatewayProperties {
private String id;
private String provider;
private String networkId;
private Map<String,Object> configuration=new HashMap<>();
}

View File

@ -0,0 +1,10 @@
package org.jetlinks.community.gateway.supports;
import reactor.core.publisher.Mono;
public interface DeviceGatewayPropertiesManager {
Mono<DeviceGatewayProperties> getProperties(String id);
}

View File

@ -0,0 +1,17 @@
package org.jetlinks.community.gateway.supports;
import org.jetlinks.community.gateway.DeviceGateway;
import org.jetlinks.community.network.NetworkType;
import reactor.core.publisher.Mono;
public interface DeviceGatewayProvider {
String getId();
String getName();
NetworkType getNetworkType();
Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties);
}

View File

@ -0,0 +1,57 @@
package org.jetlinks.community.gateway.supports;
import lombok.Getter;
import org.jetlinks.community.gateway.ClientSession;
import org.jetlinks.community.gateway.Subscription;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
class LocalClientSession implements ClientSession {
@Getter
private String id;
@Getter
private String clientId;
private Map<String, Subscription> subscriptions = new ConcurrentHashMap<>();
public LocalClientSession(String id) {
this.id = id;
this.clientId = id;
}
@Override
public boolean isPersist() {
return false;
}
@Override
public Flux<Subscription> getSubscriptions() {
return Flux.fromIterable(subscriptions.values());
}
@Override
public Mono<Void> addSubscription(Subscription subscription) {
return Mono.fromRunnable(() -> subscriptions.put(subscription.getTopic(), subscription));
}
@Override
public Mono<Void> removeSubscription(Subscription subscription) {
return Mono.fromRunnable(() -> subscriptions.remove(subscription.getTopic()));
}
@Override
public boolean isAlive() {
return true;
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,58 @@
package org.jetlinks.community.gateway.supports;
import org.jetlinks.community.gateway.ClientSession;
import org.jetlinks.community.gateway.ClientSessionManager;
import org.jetlinks.community.gateway.MessageConnection;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public class LocalClientSessionManager implements ClientSessionManager {
private Map<String, Map<String, LocalClientSession>> sessionStore = new ConcurrentHashMap<>();
protected Map<String, LocalClientSession> getGateWaySessionStore(String gateway) {
return sessionStore.computeIfAbsent(gateway, __ -> new ConcurrentHashMap<>());
}
@Override
public Mono<ClientSession> createSession(String messageGatewayId, MessageConnection connection) {
return Mono.fromSupplier(() -> {
LocalClientSession session = new LocalClientSession(connection.getId());
getGateWaySessionStore(messageGatewayId).put(connection.getId(), session);
return session;
});
}
@Override
public Flux<ClientSession> getSessions(String messageGatewayId) {
return Flux.fromIterable(getGateWaySessionStore(messageGatewayId).values());
}
@Override
public Mono<ClientSession> getSession(String messageGatewayId, String id) {
return Mono.justOrEmpty(getGateWaySessionStore(messageGatewayId).get(id));
}
@Override
public Flux<ClientSession> getSessions(String gateway, Collection<String> id) {
Map<String, LocalClientSession> store = getGateWaySessionStore(gateway);
return Flux.fromIterable(id)
.flatMap(_id -> Mono.justOrEmpty(store.get(_id)));
}
@Override
public Mono<Void> closeSession(String gateway, String id) {
return Mono.fromRunnable(() -> {
Optional.ofNullable(getGateWaySessionStore(gateway).remove(id))
.ifPresent(LocalClientSession::close);
});
}
}

View File

@ -0,0 +1,111 @@
package org.jetlinks.community.gateway.supports;
import lombok.Getter;
import org.jetlinks.community.gateway.*;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
class LocalMessageConnection implements
MessageConnection,
MessageSubscriber,
MessagePublisher {
private final List<Runnable> listener = new CopyOnWriteArrayList<>();
@Getter
private String id;
private boolean shareCluster;
private AtomicBoolean disconnected = new AtomicBoolean(false);
private EmitterProcessor<TopicMessage> processor = EmitterProcessor.create(false);
private FluxSink<TopicMessage> sink = processor.sink();
private EmitterProcessor<Subscription> subscriptionProcessor = EmitterProcessor.create(false);
private EmitterProcessor<Subscription> unsubscriptionProcessor = EmitterProcessor.create(false);
public LocalMessageConnection(String id, boolean shareCluster) {
this.id = id;
this.shareCluster = shareCluster;
}
public void addSubscription(Subscription subscription) {
subscriptionProcessor.onNext(subscription);
}
public void removeSubscription(Subscription subscription) {
unsubscriptionProcessor.onNext(subscription);
}
@Override
public void onDisconnect(Runnable disconnectListener) {
if (disconnected.get()) {
disconnectListener.run();
return;
}
listener.add(disconnectListener);
}
@Override
public void disconnect() {
listener.forEach(Runnable::run);
listener.clear();
disconnected.set(true);
processor.onComplete();
subscriptionProcessor.onComplete();
unsubscriptionProcessor.onComplete();
}
@Override
public boolean isAlive() {
return !disconnected.get();
}
@Nonnull
@Override
public Mono<Void> publish(@Nonnull TopicMessage message) {
return Mono.fromRunnable(() -> {
if (processor.hasDownstreams()) {
sink.next(message);
}
});
}
public Flux<TopicMessage> onLocalMessage() {
return processor.map(Function.identity());
}
@Nonnull
@Override
public Flux<TopicMessage> onMessage() {
return Flux.empty();
}
@Nonnull
@Override
public Flux<Subscription> onSubscribe() {
return subscriptionProcessor.map(Function.identity());
}
@Nonnull
@Override
public Flux<Subscription> onUnSubscribe() {
return unsubscriptionProcessor.map(Function.identity());
}
@Override
public boolean isShareCluster() {
return shareCluster;
}
}

View File

@ -0,0 +1,42 @@
package org.jetlinks.community.gateway.supports;
import org.jetlinks.community.gateway.MessageConnection;
import org.jetlinks.community.gateway.MessageConnector;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import javax.annotation.Nonnull;
import java.util.function.Function;
class LocalMessageConnector implements MessageConnector {
public LocalMessageConnector() {
}
@Nonnull
@Override
public String getId() {
return "local";
}
@Override
public String getName() {
return "本地连接器";
}
EmitterProcessor<MessageConnection> processor = EmitterProcessor.create(false);
public LocalMessageConnection addConnection(String id, boolean shareCluster) {
LocalMessageConnection connection = new LocalMessageConnection(id,shareCluster);
processor.onNext(connection);
return connection;
}
@Nonnull
@Override
public Flux<MessageConnection> onConnection() {
return processor.map(Function.identity());
}
}

View File

@ -0,0 +1,20 @@
package org.jetlinks.community.gateway.supports;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Getter
@Setter
public class MessageConnectorProperties {
private String id;
private String provider;
private Map<String,Object> configuration=new HashMap<>();
}

View File

@ -0,0 +1,14 @@
package org.jetlinks.community.gateway.supports;
import org.jetlinks.community.gateway.MessageConnector;
import reactor.core.publisher.Mono;
public interface MessageConnectorProvider {
String getId();
String getName();
Mono<MessageConnector> createMessageConnector(MessageConnectorProperties properties);
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jetlinks-components</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>io-component</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-core</artifactId>
<version>${hsweb.framework.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,48 @@
package org.jetlinks.community.io.excel;
import org.jetlinks.community.io.excel.easyexcel.ExcelReadDataListener;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* @author bsetfeng
* @since 1.0
**/
@Component
public class DefaultImportExportService implements ImportExportService {
public <T> Flux<RowResult<T>> doImport(Class<T> clazz, String fileUrl) {
return getInputStream(fileUrl)
.flatMapMany(inputStream -> ExcelReadDataListener.of(inputStream, clazz));
}
@Override
public <T> Flux<RowResult<T>> doImport(Class<T> clazz, InputStream stream) {
return ExcelReadDataListener.of(stream, clazz);
}
public Mono<InputStream> getInputStream(String fileUrl) {
return Mono.defer(()->{
if (fileUrl.startsWith("http")) {
return WebClient.create().get()
.uri(fileUrl)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(Resource.class))
.flatMap(resource -> Mono.fromCallable(resource::getInputStream));
} else {
return Mono.fromCallable(()->new FileInputStream(fileUrl));
}
});
}
}

View File

@ -0,0 +1,19 @@
package org.jetlinks.community.io.excel;
import reactor.core.publisher.Flux;
import java.io.InputStream;
/**
* @author bsetfeng
* @since 1.0
**/
public interface ImportExportService {
<T> Flux<RowResult<T>> doImport(Class<T> clazz, String fileUrl);
<T> Flux<RowResult<T>> doImport(Class<T> clazz, InputStream stream);
}

View File

@ -0,0 +1,14 @@
package org.jetlinks.community.io.excel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RowResult<T> {
private int rowIndex;
private T result;
}

View File

@ -0,0 +1,62 @@
package org.jetlinks.community.io.excel.easyexcel;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.community.io.excel.RowResult;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import java.io.InputStream;
/**
* 不能用spring管理每次调用都需要new
*
* @author bsetfeng
* @since 1.0
**/
@Slf4j
public class ExcelReadDataListener<T> extends AnalysisEventListener<T> {
private FluxSink<RowResult<T>> sink;
public ExcelReadDataListener(FluxSink<RowResult<T>> sink) {
this.sink = sink;
}
public static <T> Flux<RowResult<T>> of(InputStream fileInputStream, Class<T> clazz) {
return Flux.create(sink -> {
EasyExcel.read(fileInputStream, clazz, new ExcelReadDataListener<>(sink)).sheet().doRead();
});
}
@Override
public void onException(Exception exception, AnalysisContext context) {
sink.error(exception);
}
/**
* 这个每一条数据解析都会来调用
*/
@Override
public void invoke(T data, AnalysisContext analysisContext) {
RowResult<T> result=new RowResult<>();
result.setResult(data);
result.setRowIndex(analysisContext.readRowHolder().getRowIndex());
sink.next(result);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
sink.complete();
}
@Override
public boolean hasNext(AnalysisContext context) {
return !sink.isCancelled();
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>network-component</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mqtt-component</artifactId>
<dependencies>
<dependency>
<groupId>org.jetlinks</groupId>
<artifactId>rule-engine-support</artifactId>
<version>${jetlinks.version}</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt</artifactId>
</dependency>
<dependency>
<groupId>org.jetlinks</groupId>
<artifactId>jetlinks-core</artifactId>
<version>${jetlinks.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>network-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>gateway-component</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
package org.jetlinks.community.network.mqtt.client;
import org.jetlinks.core.message.codec.MqttMessage;
import org.jetlinks.community.network.Network;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
public interface MqttClient extends Network {
Flux<MqttMessage> subscribe(List<String> topics);
Mono<Void> publish(MqttMessage message);
}

View File

@ -0,0 +1,22 @@
package org.jetlinks.community.network.mqtt.client;
import io.vertx.mqtt.MqttClientOptions;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MqttClientProperties {
private String id;
private String clientId;
private String host;
private int port;
private String username;
private String password;
private String certId;
private MqttClientOptions options;
private boolean ssl;
}

View File

@ -0,0 +1,93 @@
package org.jetlinks.community.network.mqtt.client;
import io.vertx.core.Vertx;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.network.*;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.community.network.security.CertificateManager;
import org.jetlinks.community.network.security.VertxKeyCertTrustOptions;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@Component
@Slf4j
public class MqttClientProvider implements NetworkProvider<MqttClientProperties> {
private final Vertx vertx;
private final CertificateManager certificateManager;
public MqttClientProvider(CertificateManager certificateManager, Vertx vertx) {
this.vertx = vertx;
this.certificateManager = certificateManager;
}
@Nonnull
@Override
public NetworkType getType() {
return DefaultNetworkType.MQTT_CLIENT;
}
@Nonnull
@Override
public VertxMqttClient createNetwork(@Nonnull MqttClientProperties properties) {
VertxMqttClient mqttClient = new VertxMqttClient(properties.getId());
initMqttClient(mqttClient, properties);
return mqttClient;
}
@Override
public void reload(@Nonnull Network network, @Nonnull MqttClientProperties properties) {
VertxMqttClient mqttClient = ((VertxMqttClient) network);
mqttClient.shutdown();
initMqttClient(mqttClient, properties);
}
public void initMqttClient(VertxMqttClient mqttClient, MqttClientProperties properties) {
MqttClient client = MqttClient.create(vertx, properties.getOptions());
client.connect(properties.getPort(), properties.getHost(), result -> {
if (!result.succeeded()) {
log.warn("connect mqtt [{}] error", properties.getId(), result.cause());
} else {
mqttClient.setClient(client);
}
});
}
@Nullable
@Override
public ConfigMetadata getConfigMetadata() {
// TODO: 2019/12/19
return null;
}
@Nonnull
@Override
public Mono<MqttClientProperties> createConfig(@Nonnull NetworkProperties properties) {
return Mono.defer(() -> {
MqttClientProperties config = FastBeanCopier.copy(properties.getConfigurations(), new MqttClientProperties());
config.setId(properties.getId());
if (config.getOptions() == null) {
config.setOptions(new MqttClientOptions());
config.getOptions().setPassword(config.getPassword());
config.getOptions().setUsername(config.getUsername());
}
if (config.isSsl()) {
config.getOptions().setSsl(true);
return certificateManager.getCertificate(config.getCertId())
.map(VertxKeyCertTrustOptions::new)
.doOnNext(config.getOptions()::setKeyCertOptions)
.doOnNext(config.getOptions()::setTrustOptions)
.thenReturn(config);
}
return Mono.just(config);
});
}
}

View File

@ -0,0 +1,174 @@
package org.jetlinks.community.network.mqtt.client;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.buffer.Buffer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.core.message.codec.MqttMessage;
import org.jetlinks.core.message.codec.SimpleMqttMessage;
import org.jetlinks.community.network.DefaultNetworkType;
import org.jetlinks.community.network.NetworkType;
import org.jetlinks.supports.utils.MqttTopicUtils;
import reactor.core.publisher.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
public class VertxMqttClient implements MqttClient {
@Getter
private io.vertx.mqtt.MqttClient client;
private FluxProcessor<MqttMessage, MqttMessage> messageProcessor;
private FluxSink<MqttMessage> sink;
private Map<String, AtomicInteger> topicsSubscribeCounter = new ConcurrentHashMap<>();
private boolean neverSubscribe = true;
private String id;
@Getter
private AtomicInteger reloadCounter = new AtomicInteger();
public VertxMqttClient(String id) {
this.id = id;
this.messageProcessor = EmitterProcessor.create(false);
sink = this.messageProcessor.sink();
}
public void setClient(io.vertx.mqtt.MqttClient client) {
this.client = client;
if (isAlive()) {
reloadCounter.set(0);
client.publishHandler(msg -> {
//从未订阅,可能消息是还没来得及
//或者已经有了下游消费者
if (neverSubscribe || messageProcessor.hasDownstreams()) {
sink.next(SimpleMqttMessage
.builder()
.topic(msg.topicName())
.clientId(client.clientId())
.qosLevel(msg.qosLevel().value())
.retain(msg.isRetain())
.dup(msg.isDup())
.payload(msg.payload().getByteBuf())
.messageId(msg.messageId())
.build());
}
});
if (!topicsSubscribeCounter.isEmpty()) {
Map<String, Integer> reSubscribe = topicsSubscribeCounter
.entrySet()
.stream()
.filter(e -> e.getValue().get() > 0)
.map(Map.Entry::getKey)
.collect(Collectors.toMap(Function.identity(), (r) -> 0));
if (!reSubscribe.isEmpty()) {
log.info("re subscribe [{}] topic {}", client.clientId(), reSubscribe.keySet());
client.subscribe(reSubscribe);
}
}
}
}
private AtomicInteger getTopicCounter(String topic) {
return topicsSubscribeCounter.computeIfAbsent(topic, (ignore) -> new AtomicInteger());
}
@Override
public Flux<MqttMessage> subscribe(List<String> topics) {
neverSubscribe = false;
AtomicBoolean canceled = new AtomicBoolean();
return Flux.defer(() -> {
Map<String, Integer> subscribeTopic = topics.stream()
.filter(r -> getTopicCounter(r).getAndIncrement() == 0)
.collect(Collectors.toMap(Function.identity(), (r) -> 0));
if (isAlive()) {
if (!subscribeTopic.isEmpty()) {
log.info("subscribe mqtt [{}] topic : {}", client.clientId(), subscribeTopic);
client.subscribe(subscribeTopic);
}
}
return messageProcessor
.filter(msg -> topics
.stream()
.anyMatch(topic -> MqttTopicUtils.match(topic, msg.getTopic())));
}).doOnCancel(() -> {
if (!canceled.getAndSet(true)) {
for (String topic : topics) {
if (getTopicCounter(topic).decrementAndGet() <= 0 && isAlive()) {
log.info("unsubscribe mqtt [{}] topic : {}", client.clientId(), topic);
client.unsubscribe(topic);
}
}
}
});
}
@Override
public Mono<Void> publish(MqttMessage message) {
return Mono.create((sink) -> {
if (!isAlive()) {
sink.error(new IOException("mqtt client not alive"));
return;
}
Buffer buffer = Buffer.buffer(message.getPayload());
client.publish(message.getTopic(),
buffer,
MqttQoS.valueOf(message.getQosLevel()),
message.isDup(),
message.isRetain(),
result -> {
if (result.succeeded()) {
log.info("publish mqtt [{}] message success: {}", client.clientId(), message);
sink.success();
} else {
log.info("publish mqtt [{}] message error : {}", client.clientId(), message, result.cause());
sink.error(result.cause());
}
});
});
}
@Override
public String getId() {
return id;
}
@Override
public NetworkType getType() {
return DefaultNetworkType.MQTT_CLIENT;
}
@Override
public void shutdown() {
if (isAlive()) {
client.disconnect();
client = null;
}
}
@Override
public boolean isAlive() {
return client != null && client.isConnected();
}
@Override
public boolean isAutoReload() {
return true;
}
}

View File

@ -0,0 +1,199 @@
package org.jetlinks.community.network.mqtt.gateway.device;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.core.ProtocolSupport;
import org.jetlinks.core.ProtocolSupports;
import org.jetlinks.core.device.*;
import org.jetlinks.core.message.DeviceMessage;
import org.jetlinks.core.message.DeviceOfflineMessage;
import org.jetlinks.core.message.DeviceOnlineMessage;
import org.jetlinks.core.message.Message;
import org.jetlinks.core.message.codec.*;
import org.jetlinks.core.server.MessageHandler;
import org.jetlinks.community.gateway.DeviceGateway;
import org.jetlinks.community.network.DefaultNetworkType;
import org.jetlinks.community.network.NetworkType;
import org.jetlinks.community.network.mqtt.client.MqttClient;
import org.jetlinks.supports.server.DecodedClientMessageHandler;
import reactor.core.Disposable;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
@Slf4j
public class MqttClientDeviceGateway implements DeviceGateway {
@Getter
private String id;
private MqttClient mqttClient;
private DeviceRegistry registry;
private List<String> topics;
private String protocol;
private ProtocolSupports protocolSupport;
private DecodedClientMessageHandler clientMessageHandler;
private MessageHandler messageHandler;
private EmitterProcessor<Message> messageProcessor = EmitterProcessor.create(false);
private FluxSink<Message> sink = messageProcessor.sink();
private AtomicBoolean started = new AtomicBoolean();
private List<Disposable> disposable = new CopyOnWriteArrayList<>();
public MqttClientDeviceGateway(String id,
MqttClient mqttClient,
DeviceRegistry registry,
ProtocolSupports protocolSupport,
String protocol,
DecodedClientMessageHandler clientMessageHandler,
MessageHandler messageHandler,
List<String> topics) {
this.id = Objects.requireNonNull(id, "id");
this.mqttClient = Objects.requireNonNull(mqttClient, "mqttClient");
this.registry = Objects.requireNonNull(registry, "registry");
this.protocolSupport = Objects.requireNonNull(protocolSupport, "protocolSupport");
this.protocol = Objects.requireNonNull(protocol, "protocol");
this.clientMessageHandler = Objects.requireNonNull(clientMessageHandler, "clientMessageHandler");
this.messageHandler = Objects.requireNonNull(messageHandler, "messageHandler");
this.topics = Objects.requireNonNull(topics, "topics");
}
protected Mono<ProtocolSupport> getProtocol() {
return protocolSupport.getProtocol(protocol);
}
private void doStart() {
if (started.getAndSet(true) || !disposable.isEmpty()) {
return;
}
messageHandler
.handleGetDeviceState(getId(), idPublisher ->
Flux.from(idPublisher)
.map(id -> new DeviceStateInfo(id, DeviceState.online)));
disposable.add(messageHandler
.handleSendToDeviceMessage(getId())
.filter((msg) -> started.get())
.flatMap(msg -> {
if (msg instanceof DeviceMessage) {
DeviceMessage deviceMessage = ((DeviceMessage) msg);
return registry.getDevice(deviceMessage.getDeviceId())
.flatMapMany(device -> device.getProtocol()
.flatMapMany(protocol ->
protocol.getMessageCodec(getTransport())
.flatMapMany(codec -> codec.encode(new MessageEncodeContext() {
@Override
public Message getMessage() {
return deviceMessage;
}
@Override
public DeviceOperator getDevice() {
return device;
}
}))))
.flatMap(message -> mqttClient.publish(((MqttMessage) message)));
}
return Mono.empty();
})
.onErrorContinue((err, res) -> log.error("处理MQTT消息失败", err))
.subscribe());
disposable.add(mqttClient
.subscribe(topics)
.filter((msg) -> started.get())
.flatMap(mqttMessage -> getProtocol()
.flatMap(codec -> codec.getMessageCodec(getTransport()))
.flatMapMany(codec -> codec.decode(new MessageDecodeContext() {
@Override
public EncodedMessage getMessage() {
return mqttMessage;
}
@Override
public DeviceOperator getDevice() {
throw new UnsupportedOperationException();
}
}))
.cast(DeviceMessage.class)
.flatMap(msg -> {
if (messageProcessor.hasDownstreams()) {
sink.next(msg);
}
return registry
.getDevice(msg.getDeviceId())
.flatMap(device -> {
Mono<Void> handle = clientMessageHandler.handleMessage(device, msg).then();
if (msg instanceof DeviceOfflineMessage) {
handle = handle.then(device.offline().then());
}
if (msg instanceof DeviceOnlineMessage) {
handle = handle.then(device.online(getId(), getId()).then());
}
return handle;
});
}))
.onErrorContinue((err, res) -> log.error("处理MQTT消息失败", err))
.subscribe());
}
@Override
public Transport getTransport() {
return DefaultTransport.MQTT;
}
@Override
public NetworkType getNetworkType() {
return DefaultNetworkType.MQTT_CLIENT;
}
@Override
public Flux<Message> onMessage() {
return messageProcessor.map(Function.identity());
}
@Override
public Mono<Void> pause() {
return Mono.fromRunnable(() -> started.set(false));
}
@Override
public Mono<Void> startup() {
return Mono.fromRunnable(this::doStart);
}
@Override
public Mono<Void> shutdown() {
return Mono.fromRunnable(() -> {
started.set(false);
disposable.forEach(Disposable::dispose);
disposable.clear();
});
}
@Override
public boolean isAlive() {
return started.get();
}
}

View File

@ -0,0 +1,82 @@
package org.jetlinks.community.network.mqtt.gateway.device;
import org.jetlinks.core.ProtocolSupports;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.server.MessageHandler;
import org.jetlinks.community.gateway.DeviceGateway;
import org.jetlinks.community.gateway.supports.DeviceGatewayProperties;
import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
import org.jetlinks.community.network.DefaultNetworkType;
import org.jetlinks.community.network.NetworkManager;
import org.jetlinks.community.network.NetworkType;
import org.jetlinks.community.network.mqtt.client.MqttClient;
import org.jetlinks.supports.server.DecodedClientMessageHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Objects;
@Component
public class MqttClientDeviceGatewayProvider implements DeviceGatewayProvider {
private final NetworkManager networkManager;
private final DeviceRegistry registry;
private final MessageHandler messageHandler;
private final DecodedClientMessageHandler clientMessageHandler;
private final ProtocolSupports protocolSupports;
public MqttClientDeviceGatewayProvider(NetworkManager networkManager,
DeviceRegistry registry,
MessageHandler messageHandler,
DecodedClientMessageHandler clientMessageHandler,
ProtocolSupports protocolSupports) {
this.networkManager = networkManager;
this.registry = registry;
this.messageHandler = messageHandler;
this.clientMessageHandler = clientMessageHandler;
this.protocolSupports = protocolSupports;
}
@Override
public String getId() {
return "mqtt-client-gateway";
}
@Override
public String getName() {
return "MQTT客户端设备网关";
}
@Override
public NetworkType getNetworkType() {
return DefaultNetworkType.MQTT_CLIENT;
}
@Override
public Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties) {
return networkManager
.<MqttClient>getNetwork(getNetworkType(), properties.getNetworkId())
.map(mqttClient -> {
String protocol = (String) properties.getConfiguration().get("protocol");
String topics = (String) properties.getConfiguration().get("topics");
Objects.requireNonNull(topics, "topics");
return new MqttClientDeviceGateway(properties.getId(),
mqttClient,
registry,
protocolSupports,
protocol,
clientMessageHandler,
messageHandler,
Arrays.asList(topics.split("[,;\n]"))
);
});
}
}

View File

@ -0,0 +1,194 @@
package org.jetlinks.community.network.mqtt.gateway.device;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.core.device.AuthenticationResponse;
import org.jetlinks.core.device.DeviceOperator;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.device.MqttAuthenticationRequest;
import org.jetlinks.core.message.Message;
import org.jetlinks.core.message.codec.DefaultTransport;
import org.jetlinks.core.message.codec.EncodedMessage;
import org.jetlinks.core.message.codec.FromDeviceMessageContext;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.core.server.session.DeviceSession;
import org.jetlinks.core.server.session.DeviceSessionManager;
import org.jetlinks.community.gateway.DeviceGateway;
import org.jetlinks.community.network.DefaultNetworkType;
import org.jetlinks.community.network.NetworkType;
import org.jetlinks.community.network.mqtt.gateway.device.session.MqttConnectionSession;
import org.jetlinks.community.network.mqtt.server.MqttConnection;
import org.jetlinks.community.network.mqtt.server.MqttServer;
import org.jetlinks.supports.server.DecodedClientMessageHandler;
import org.springframework.util.StringUtils;
import reactor.core.Disposable;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuples;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
@Slf4j
class MqttServerDeviceGateway implements DeviceGateway {
@Getter
private String id;
private DeviceRegistry registry;
private DeviceSessionManager sessionManager;
private MqttServer mqttServer;
private DecodedClientMessageHandler messageHandler;
public MqttServerDeviceGateway(String id,
DeviceRegistry registry,
DeviceSessionManager sessionManager,
MqttServer mqttServer,
DecodedClientMessageHandler messageHandler) {
this.id = id;
this.registry = registry;
this.sessionManager = sessionManager;
this.mqttServer = mqttServer;
this.messageHandler = messageHandler;
}
private EmitterProcessor<Message> messageProcessor = EmitterProcessor.create(false);
private FluxSink<Message> sink = messageProcessor.sink();
private AtomicBoolean started = new AtomicBoolean();
private Disposable disposable;
private void doStart() {
if (started.getAndSet(true) || disposable != null) {
return;
}
disposable = mqttServer
.handleConnection()
.filter(conn -> {
if (!started.get()) {
conn.reject(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE);
}
return started.get();
})
.flatMap(con -> Mono.justOrEmpty(con.getAuth())
//没有认证信息,则拒绝连接.
.switchIfEmpty(Mono.fromRunnable(() -> con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED)))
.flatMap(auth ->
registry.getDevice(con.getClientId())
.flatMap(device -> device
.authenticate(new MqttAuthenticationRequest(con.getClientId(), auth.getUsername(), auth.getPassword(), getTransport()))
.switchIfEmpty(Mono.fromRunnable(() -> con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)))
.flatMap(resp -> {
String deviceId = StringUtils.isEmpty(resp.getDeviceId()) ? device.getDeviceId() : resp.getDeviceId();
//认证返回了新的设备ID,则使用新的设备
if (!deviceId.equals(device.getDeviceId())) {
return registry
.getDevice(deviceId)
.map(operator -> Tuples.of(operator, resp, con));
}
return Mono.just(Tuples.of(device, resp, con));
})
))
//设备注册信息不存在,拒绝连接
.switchIfEmpty(Mono.fromRunnable(() -> con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED)))
.onErrorContinue((err, res) -> {
con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE);
log.error("MQTT连接认证[{}]失败", con.getClientId(), err);
}))
.flatMap(tuple3 -> {
DeviceOperator device = tuple3.getT1();
AuthenticationResponse resp = tuple3.getT2();
MqttConnection con = tuple3.getT3();
String deviceId = device.getDeviceId();
if (resp.isSuccess()) {
DeviceSession session = new MqttConnectionSession(deviceId, device, getTransport(), con);
sessionManager.register(session);
con.onClose(conn -> sessionManager.unregister(deviceId));
return Mono.just(Tuples.of(con.accept(), device, session));
} else {
log.warn("MQTT客户端认证[{}]失败:{}", deviceId, resp.getMessage());
}
return Mono.empty();
})
.onErrorContinue((err, res) -> log.error("处理MQTT连接失败", err))
.subscribe(tp -> tp.getT1()
.handleMessage()
.filter(pb -> started.get())
.takeWhile(pub -> disposable != null)
.flatMap(publishing -> tp.getT2()
.getProtocol()
.flatMap(protocol -> protocol.getMessageCodec(getTransport()))
.flatMapMany(codec -> codec.decode(new FromDeviceMessageContext() {
@Override
public DeviceSession getSession() {
return tp.getT3();
}
@Override
public EncodedMessage getMessage() {
return publishing.getMessage();
}
}))
.flatMap(msg -> {
if (messageProcessor.hasDownstreams()) {
sink.next(msg);
}
return messageHandler.handleMessage(tp.getT2(), msg);
})
.onErrorContinue((err, res) -> log.error("处理MQTT连接[{}]消息失败:{}", tp.getT2().getDeviceId(), publishing.getMessage(), err)))
.subscribe()
);
}
@Override
public Transport getTransport() {
return DefaultTransport.MQTT;
}
@Override
public NetworkType getNetworkType() {
return DefaultNetworkType.MQTT_SERVER;
}
@Override
public Flux<Message> onMessage() {
return messageProcessor
.map(Function.identity());
}
@Override
public Mono<Void> pause() {
return Mono.fromRunnable(() -> started.set(false));
}
@Override
public Mono<Void> startup() {
return Mono.fromRunnable(this::doStart);
}
@Override
public Mono<Void> shutdown() {
return Mono.fromRunnable(() -> {
started.set(false);
if (disposable != null && !disposable.isDisposed()) {
disposable.dispose();
}
disposable = null;
});
}
@Override
public boolean isAlive() {
return started.get();
}
}

View File

@ -0,0 +1,62 @@
package org.jetlinks.community.network.mqtt.gateway.device;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.server.session.DeviceSessionManager;
import org.jetlinks.community.gateway.DeviceGateway;
import org.jetlinks.community.gateway.supports.DeviceGatewayProperties;
import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
import org.jetlinks.community.network.DefaultNetworkType;
import org.jetlinks.community.network.NetworkManager;
import org.jetlinks.community.network.NetworkType;
import org.jetlinks.community.network.mqtt.server.MqttServer;
import org.jetlinks.supports.server.DecodedClientMessageHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class MqttServerDeviceGatewayProvider implements DeviceGatewayProvider {
private final NetworkManager networkManager;
private final DeviceRegistry registry;
private final DeviceSessionManager sessionManager;
private final DecodedClientMessageHandler messageHandler;
public MqttServerDeviceGatewayProvider(NetworkManager networkManager,
DeviceRegistry registry,
DeviceSessionManager sessionManager,
DecodedClientMessageHandler messageHandler) {
this.networkManager = networkManager;
this.registry = registry;
this.sessionManager = sessionManager;
this.messageHandler = messageHandler;
}
@Override
public String getId() {
return "mqtt-server-gateway";
}
@Override
public String getName() {
return "MQTT服务设备网关";
}
@Override
public NetworkType getNetworkType() {
return DefaultNetworkType.MQTT_SERVER;
}
@Override
public Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties) {
return networkManager
.<MqttServer>getNetwork(getNetworkType(), properties.getNetworkId())
.map(mqttServer -> {
MqttServerDeviceGateway gateway = new MqttServerDeviceGateway(properties.getId(), registry, sessionManager, mqttServer, messageHandler);
return gateway;
});
}
}

View File

@ -0,0 +1,75 @@
package org.jetlinks.community.network.mqtt.gateway.device.session;
import lombok.Getter;
import org.jetlinks.core.device.DeviceOperator;
import org.jetlinks.core.message.codec.EncodedMessage;
import org.jetlinks.core.message.codec.MqttMessage;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.core.server.session.DeviceSession;
import org.jetlinks.community.network.mqtt.server.MqttConnection;
import reactor.core.publisher.Mono;
public class MqttConnectionSession implements DeviceSession {
@Getter
private String id;
@Getter
private DeviceOperator operator;
@Getter
private Transport transport;
@Getter
private MqttConnection connection;
public MqttConnectionSession(String id,DeviceOperator operator,Transport transport,MqttConnection connection){
this.id=id;
this.operator=operator;
this.transport=transport;
this.connection=connection;
}
private long connectTime = System.currentTimeMillis();
@Override
public String getDeviceId() {
return id;
}
@Override
public long lastPingTime() {
return connection.getLastPingTime();
}
@Override
public long connectTime() {
return connectTime;
}
@Override
public Mono<Boolean> send(EncodedMessage encodedMessage) {
return Mono.defer(() -> connection.publish(((MqttMessage) encodedMessage)))
.thenReturn(true);
}
@Override
public void close() {
connection.close().subscribe();
}
@Override
public void ping() {
}
@Override
public boolean isAlive() {
return connection.isAlive();
}
@Override
public void onClose(Runnable call) {
}
}

Some files were not shown because too many files have changed in this diff Show More