first commit
This commit is contained in:
commit
d854b56436
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
// }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 + "的枚举");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# 网关模块
|
||||
|
||||
统一管理设备网关服务,消息网关.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<>();
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package org.jetlinks.community.gateway.supports;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface DeviceGatewayPropertiesManager {
|
||||
|
||||
Mono<DeviceGatewayProperties> getProperties(String id);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<>();
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4" />
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4" />
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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]"))
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue