Merge pull request #204 from jetlinks/fix-bug

同步协议模块代码
This commit is contained in:
老周 2022-10-18 18:22:29 +08:00 committed by GitHub
commit 1a25e693b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 500 additions and 121 deletions

View File

@ -24,6 +24,8 @@ public interface DataReferenceManager {
String TYPE_NETWORK = "network";
//数据类型关系配置
String TYPE_RELATION = "relation";
//数据类型消息协议
String TYPE_PROTOCOL = "protocol";
/**
* 判断指定数据类型的数据是否已经被其他地方所引用

View File

@ -0,0 +1,122 @@
package org.jetlinks.community.auth.initialize;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.authorization.DefaultDimensionType;
import org.hswebframework.web.authorization.Permission;
import org.hswebframework.web.authorization.events.AuthorizationInitializeEvent;
import org.hswebframework.web.authorization.simple.SimpleAuthentication;
import org.hswebframework.web.authorization.simple.SimplePermission;
import org.hswebframework.web.system.authorization.api.entity.ActionEntity;
import org.hswebframework.web.system.authorization.api.entity.PermissionEntity;
import org.hswebframework.web.system.authorization.defaults.service.DefaultPermissionService;
import org.jetlinks.community.auth.entity.MenuEntity;
import org.jetlinks.community.auth.entity.MenuView;
import org.jetlinks.community.auth.service.DefaultMenuService;
import org.jetlinks.community.auth.service.request.MenuGrantRequest;
import org.jetlinks.community.auth.web.request.AuthorizationSettingDetail;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@AllArgsConstructor
@Component
public class MenuAuthenticationInitializeService {
private final DefaultMenuService menuService;
private final DefaultPermissionService permissionService;
/**
* 根据角色配置的菜单权限来重构权限信息
*
* @param event 权限初始化事件
*/
@EventListener
public void refactorPermission(AuthorizationInitializeEvent event) {
if (event.getAuthentication().getDimensions().isEmpty()) {
return;
}
event.async(
Mono
.zip(
// T1: 权限定义列表
permissionService
.createQuery()
.where(PermissionEntity::getStatus, 1)
.fetch()
.collectMap(PermissionEntity::getId, Function.identity()),
// T2: 菜单定义列表
menuService
.createQuery()
.where(MenuEntity::getStatus, 1)
.fetch()
.collectList(),
// T3: 角色赋予的菜单列表
menuService
.getGrantedMenus(QueryParamEntity.of(), event
.getAuthentication()
.getDimensions())
.collectList()
.filter(CollectionUtils::isNotEmpty)
)
.<Permission>flatMapIterable(tp3 -> {
Map<String, PermissionEntity> permissions = tp3.getT1();
List<MenuEntity> menus = tp3.getT2();
List<MenuView> grantedMenus = tp3.getT3();
MenuGrantRequest request = new MenuGrantRequest();
request.setTargetType(DefaultDimensionType.role.getId());
request.setTargetId("merge");
request.setMenus(grantedMenus);
AuthorizationSettingDetail detail = request.toAuthorizationSettingDetail(menus);
return detail
.getPermissionList()
.stream()
.map(per -> {
PermissionEntity entity = permissions.get(per.getId());
if (entity == null || per.getActions() == null) {
return null;
}
Set<String> actions;
if (CollectionUtils.isEmpty(entity.getActions())) {
actions = new HashSet<>();
} else {
Set<String> defActions = entity
.getActions()
.stream()
.map(ActionEntity::getAction)
.collect(Collectors.toSet());
actions = new HashSet<>(per.getActions());
actions.retainAll(defActions);
}
return SimplePermission
.builder()
.id(entity.getId())
.name(entity.getName())
.options(entity.getProperties())
.actions(actions)
.build();
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
})
.collectList()
.filter(CollectionUtils::isNotEmpty)
.doOnNext(mapping -> {
SimpleAuthentication authentication = new SimpleAuthentication();
authentication.setUser(event.getAuthentication().getUser());
authentication.setPermissions(mapping);
event.setAuthentication(event.getAuthentication().merge(authentication));
})
);
}
}

View File

@ -1,30 +0,0 @@
package org.jetlinks.community.device.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity;
import java.util.List;
@Getter
@Setter
public class DeviceCategory extends GenericTreeSortSupportEntity<String> {
@Schema(description = "ID")
private String id;
@Schema(description = "标识")
private String key;
@Schema(description = "名称")
private String name;
@Schema(description = "父节点标识")
private String parentId;
@Schema(description = "子节点")
private List<DeviceCategory> children;
}

View File

@ -0,0 +1,79 @@
package org.jetlinks.community.device.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity;
import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
import org.hswebframework.web.crud.annotation.EnableEntityEvent;
import org.hswebframework.web.crud.generator.Generators;
import org.hswebframework.web.validator.CreateGroup;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.sql.JDBCType;
import java.util.List;
@Getter
@Setter
@Table(name = "dev_product_category")
@Comment("产品分类信息表")
@EnableEntityEvent
public class DeviceCategoryEntity extends GenericTreeSortSupportEntity<String> implements RecordCreationEntity {
@Override
@Id
@Column(length = 64, updatable = false)
@GeneratedValue(generator = Generators.SNOW_FLAKE)
@NotBlank(message = "ID不能为空", groups = CreateGroup.class)
@Pattern(regexp = "^[0-9a-zA-Z_\\-|]+$", message = "ID只能由数字,字母,下划线和中划线组成", groups = CreateGroup.class)
public String getId() {
return super.getId();
}
@Schema(description = "标识")
@Column(nullable = false,length = 64)
@NotBlank(message = "标识不能为空", groups = CreateGroup.class)
@GeneratedValue(generator = Generators.SNOW_FLAKE)
@Pattern(regexp = "^[0-9a-zA-Z_\\-]+$", message = "分类标识只能由数字,字母,下划线和中划线组成")
private String key;
@Schema(description = "名称")
@Column(nullable = false)
@NotBlank
private String name;
@Schema(description = "说明")
@Column
private String description;
@Schema(description = "子节点")
private List<DeviceCategoryEntity> children;
@Schema(description = "物模型")
@Column
@ColumnType(javaType = String.class, jdbcType = JDBCType.CLOB)
private String metadata;
@Column(updatable = false)
@Schema(
description = "创建者ID(只读)"
, accessMode = Schema.AccessMode.READ_ONLY
)
private String creatorId;
@Column(updatable = false)
@DefaultValue(generator = Generators.CURRENT_TIME)
@Schema(
description = "创建时间(只读)"
, accessMode = Schema.AccessMode.READ_ONLY
)
private Long createTime;
}

View File

@ -0,0 +1,80 @@
package org.jetlinks.community.device.service;
import com.alibaba.fastjson.JSON;
import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
import org.hswebframework.web.crud.service.GenericReactiveTreeSupportCrudService;
import org.hswebframework.web.id.IDGenerator;
import org.jetlinks.community.device.entity.DeviceCategoryEntity;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Service
public class DeviceCategoryService extends GenericReactiveTreeSupportCrudService<DeviceCategoryEntity, String> implements CommandLineRunner {
@Override
public IDGenerator<String> getIDGenerator() {
return IDGenerator.MD5;
}
private static final String category_splitter = "-";
@Override
public void setChildren(DeviceCategoryEntity entity, List<DeviceCategoryEntity> children) {
entity.setChildren(children);
}
@Override
public void run(String... args) {
this
.createQuery()
.fetchOne()
.switchIfEmpty(initDefaultData().then(Mono.empty()))
.subscribe();
}
static void rebuild(String parentId, List<DeviceCategoryEntity> children) {
if (children == null) {
return;
}
for (DeviceCategoryEntity child : children) {
String id = child.getId();
child.setId(parentId + category_splitter + id +category_splitter);
child.setParentId(parentId +category_splitter);
rebuild(parentId + category_splitter + id, child.getChildren());
}
}
private Mono<Void> initDefaultData() {
return Mono
.fromCallable(() -> {
ClassPathResource resource = new ClassPathResource("device-category.json");
try (InputStream stream = resource.getInputStream()) {
String json = StreamUtils.copyToString(stream, StandardCharsets.UTF_8);
List<DeviceCategoryEntity> all = JSON.parseArray(json, DeviceCategoryEntity.class);
List<DeviceCategoryEntity> root = TreeSupportEntity.list2tree(all, DeviceCategoryEntity::setChildren);
for (DeviceCategoryEntity category : root) {
String id = category.getId();
category.setId(category_splitter + id + category_splitter);
category.setParentId(category_splitter + category.getParentId() + category_splitter);
rebuild(category_splitter + id, category.getChildren());
}
return root;
}
})
.flatMap(all -> save(Flux.fromIterable(all)))
.then();
}
}

View File

@ -1,48 +1,55 @@
package org.jetlinks.community.device.service;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.crud.service.GenericReactiveCrudService;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.exception.NotFoundException;
import org.jetlinks.community.device.entity.ProtocolSupportEntity;
import org.jetlinks.supports.protocol.management.ProtocolSupportLoader;
import org.jetlinks.community.reference.DataReferenceManager;
import org.jetlinks.supports.protocol.management.ProtocolSupportManager;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@Slf4j
public class LocalProtocolSupportService extends GenericReactiveCrudService<ProtocolSupportEntity, String> {
@Autowired
private ProtocolSupportManager supportManager;
@Autowired
private ProtocolSupportLoader loader;
private DataReferenceManager referenceManager;
@Override
public Mono<Integer> deleteById(Publisher<String> idPublisher) {
return Flux.from(idPublisher)
.flatMap(id -> supportManager.remove(id).thenReturn(id))
.as(super::deleteById);
}
public Mono<Boolean> deploy(String id) {
return findById(Mono.just(id))
.switchIfEmpty(Mono.error(NotFoundException::new))
.map(ProtocolSupportEntity::toDeployDefinition)
.flatMap(def->loader.load(def).thenReturn(def))
.onErrorMap(err->new BusinessException("无法加载协议:"+err.getMessage(),err))
.flatMap(supportManager::save)
.flatMap(r -> createUpdate()
.set(ProtocolSupportEntity::getState, 1)
.where(ProtocolSupportEntity::getId, id)
.execute())
.map(i -> i > 0);
.switchIfEmpty(Mono.error(NotFoundException::new))
.flatMap(r -> createUpdate()
.set(ProtocolSupportEntity::getState, 1)
.where(ProtocolSupportEntity::getId, id)
.execute())
.map(i -> i > 0);
}
public Mono<Boolean> unDeploy(String id) {
return findById(Mono.just(id))
.switchIfEmpty(Mono.error(NotFoundException::new))
.map(ProtocolSupportEntity::toUnDeployDefinition)
.flatMap(supportManager::save)
.flatMap(r -> createUpdate()
.set(ProtocolSupportEntity::getState, 0)
.where(ProtocolSupportEntity::getId, id)
.execute())
.map(i -> i > 0);
// 消息协议被使用时不能禁用
return referenceManager
.assertNotReferenced(DataReferenceManager.TYPE_PROTOCOL, id)
.then(findById(Mono.just(id)))
.switchIfEmpty(Mono.error(NotFoundException::new))
.flatMap(r -> createUpdate()
.set(ProtocolSupportEntity::getState, 0)
.where(ProtocolSupportEntity::getId, id)
.execute())
.map(i -> i > 0);
}
}

View File

@ -0,0 +1,73 @@
package org.jetlinks.community.device.service;
import lombok.AllArgsConstructor;
import org.hswebframework.web.crud.events.EntityBeforeDeleteEvent;
import org.hswebframework.web.crud.events.EntityCreatedEvent;
import org.hswebframework.web.crud.events.EntityModifyEvent;
import org.hswebframework.web.crud.events.EntitySavedEvent;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.device.entity.ProtocolSupportEntity;
import org.jetlinks.community.reference.DataReferenceManager;
import org.jetlinks.core.ProtocolSupport;
import org.jetlinks.supports.protocol.management.ProtocolSupportLoader;
import org.jetlinks.supports.protocol.management.ProtocolSupportManager;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
/**
* 协议事件处理类.
*
* @author zhangji 2022/4/1
*/
@Component
@AllArgsConstructor
public class ProtocolSupportHandler {
private final DataReferenceManager referenceManager;
private ProtocolSupportLoader loader;
private ProtocolSupportManager supportManager;
//禁止删除已有网关使用的协议
@EventListener
public void handleProtocolDelete(EntityBeforeDeleteEvent<ProtocolSupportEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(protocol -> referenceManager
.assertNotReferenced(DataReferenceManager.TYPE_PROTOCOL, protocol.getId()))
);
}
@EventListener
public void handleCreated(EntityCreatedEvent<ProtocolSupportEntity> event) {
event.async(reloadProtocol(event.getEntity()));
}
@EventListener
public void handleSaved(EntitySavedEvent<ProtocolSupportEntity> event) {
event.async(reloadProtocol(event.getEntity()));
}
@EventListener
public void handleModify(EntityModifyEvent<ProtocolSupportEntity> event) {
event.async(reloadProtocol(event.getAfter()));
}
// 重新加载协议
private Mono<Void> reloadProtocol(Collection<ProtocolSupportEntity> protocol) {
return Flux
.fromIterable(protocol)
.filter(entity -> entity.getState() != null)
.map(entity -> entity.getState() == 1 ? entity.toDeployDefinition() : entity.toUnDeployDefinition())
.flatMap(def -> loader
//加载一下检验是否正确然后就卸载
.load(def)
.doOnNext(ProtocolSupport::dispose)
.thenReturn(def))
.onErrorMap(err -> new BusinessException("error.unable_to_load_protocol", 500, err.getMessage()))
.flatMap(supportManager::save)
.then();
}
}

View File

@ -1,85 +1,72 @@
package org.jetlinks.community.device.web;
import com.alibaba.fastjson.JSON;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
import org.jetlinks.community.device.entity.DeviceCategory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.api.crud.entity.QueryNoPagingOperation;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.jetlinks.community.device.entity.DeviceCategoryEntity;
import org.jetlinks.community.device.service.DeviceCategoryService;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/device/category")
@Slf4j
@Tag(name = "设备分类目录")
public class DeviceCategoryController {
@Tag(name = "产品分类管理")
@AllArgsConstructor
@Resource(id="device-category",name = "产品分类")
public class DeviceCategoryController implements ReactiveServiceCrudController<DeviceCategoryEntity,String> {
static List<DeviceCategory> statics;
static void rebuild(String parentId, List<DeviceCategory> children) {
if (children == null) {
return;
}
for (DeviceCategory child : children) {
String id = child.getId();
child.setId(parentId + "|" + id + "|");
child.setParentId(parentId + "|");
rebuild(parentId + "|" + id, child.getChildren());
}
}
static {
try {
ClassPathResource resource = new ClassPathResource("device-category.json");
String json = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
List<DeviceCategory> all = JSON.parseArray(json, DeviceCategory.class);
List<DeviceCategory> root = TreeSupportEntity.list2tree(all, DeviceCategory::setChildren);
for (DeviceCategory category : root) {
String id = category.getId();
category.setId("|" + id + "|");
category.setParentId("|" + category.getParentId() + "|");
rebuild("|" + id, category.getChildren());
}
statics = all;
} catch (Exception e) {
statics = new ArrayList<>();
DeviceCategoryController.log.error(e.getMessage(), e);
}
}
private final DeviceCategoryService categoryService;
@GetMapping
@Operation(summary = "获取全部分类目录")
public Flux<DeviceCategory> getAllCategory() {
return Flux.fromIterable(statics);
@QueryNoPagingOperation(summary = "获取全部分类")
@Authorize(merge = false)
public Flux<DeviceCategoryEntity> getAllCategory(@Parameter(hidden = true) QueryParamEntity query) {
return this
.categoryService
.createQuery()
.setParam(query)
.fetch();
}
@GetMapping("/_query/no-paging")
@Operation(summary = "获取全部分类目录")
public Flux<DeviceCategory> getAllCategory2() {
return Flux.fromIterable(statics);
}
@GetMapping("/_tree")
@Operation(summary = "获取全部分类目录(树结构)")
public Flux<DeviceCategory> getAllCategoryTree() {
return Flux.fromIterable(TreeSupportEntity.list2tree(statics, DeviceCategory::setChildren));
@QueryNoPagingOperation(summary = "获取全部分类(树结构)")
@Authorize(merge = false)
public Flux<DeviceCategoryEntity> getAllCategoryTree(@Parameter(hidden = true) QueryParamEntity query) {
return this
.categoryService
.createQuery()
.setParam(query)
.fetch()
.collectList()
.flatMapMany(all-> Flux.fromIterable(TreeSupportEntity.list2tree(all, DeviceCategoryEntity::setChildren)));
}
@PostMapping("/_tree")
@QueryNoPagingOperation(summary = "获取全部分类(树结构)")
@Authorize(merge = false)
public Flux<DeviceCategoryEntity> getAllCategoryTreeByQueryParam(@RequestBody Mono<QueryParamEntity> query) {
return this
.categoryService
.query(query)
.collectList()
.flatMapMany(all-> Flux.fromIterable(TreeSupportEntity.list2tree(all, DeviceCategoryEntity::setChildren)));
}
@Override
public ReactiveCrudService<DeviceCategoryEntity, String> getService() {
return categoryService;
}
}

View File

@ -7,11 +7,13 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Getter;
import org.hswebframework.utils.StringUtils;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.annotation.QueryAction;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.authorization.annotation.SaveAction;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.device.entity.ProtocolSupportEntity;
import org.jetlinks.community.device.service.LocalProtocolSupportService;
import org.jetlinks.community.device.web.protocol.ProtocolDetail;
@ -19,6 +21,7 @@ import org.jetlinks.community.device.web.protocol.ProtocolInfo;
import org.jetlinks.community.device.web.protocol.TransportInfo;
import org.jetlinks.community.device.web.request.ProtocolDecodeRequest;
import org.jetlinks.community.device.web.request.ProtocolEncodeRequest;
import org.jetlinks.community.protocol.TransportDetail;
import org.jetlinks.core.ProtocolSupport;
import org.jetlinks.core.ProtocolSupports;
import org.jetlinks.core.message.codec.Transport;
@ -32,7 +35,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.util.Comparator;
import java.util.List;
@RestController
@ -174,4 +180,57 @@ public class ProtocolSupportController
public Flux<ValueUnit> allUnits() {
return Flux.fromIterable(ValueUnits.getAllUnit());
}
@GetMapping("/supports/{transport}")
@Authorize(merge = false)
@Operation(summary = "获取支持指定传输协议的消息协议")
public Flux<ProtocolInfo> getSupportTransportProtocols(@PathVariable String transport,
@Parameter(hidden = true) QueryParamEntity query) {
return protocolSupports
.getProtocols()
.collectMap(ProtocolSupport::getId)
.flatMapMany(protocols -> service.createQuery()
.setParam(query)
.fetch()
.index()
.flatMap(tp2 -> Mono
.justOrEmpty(protocols.get(tp2.getT2().getId()))
.filterWhen(support -> support
.getSupportedTransport()
.filter(t -> t.isSame(transport))
.hasElements())
.map(ProtocolInfo::of)
.map(protocolInfo -> Tuples.of(tp2.getT1(), protocolInfo))))
.sort(Comparator.comparingLong(Tuple2::getT1))
.map(Tuple2::getT2);
}
@GetMapping("/{id}/transport/{transport}")
@Authorize(merge = false)
@Operation(summary = "获取消息协议对应的传输协议信息")
public Mono<TransportDetail> getTransportDetail(@PathVariable @Parameter(description = "协议ID") String id,
@PathVariable @Parameter(description = "传输协议") String transport) {
return protocolSupports
.getProtocol(id)
.onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id))
.flatMapMany(protocol -> protocol
.getSupportedTransport()
.filter(trans -> trans.isSame(transport))
.distinct()
.flatMap(_transport -> TransportDetail.of(protocol, _transport)))
.singleOrEmpty();
}
@PostMapping("/{id}/detail")
@QueryAction
@Operation(summary = "获取协议详情")
public Mono<ProtocolDetail> protocolDetail(@PathVariable String id) {
return protocolSupports
.getProtocol(id)
.onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id))
.flatMap(ProtocolDetail::of);
}
}