|
@@ -1,5 +1,10 @@
|
|
package com.provectus.kafka.ui.controller;
|
|
package com.provectus.kafka.ui.controller;
|
|
|
|
|
|
|
|
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.CREATE;
|
|
|
|
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.DELETE;
|
|
|
|
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.EDIT;
|
|
|
|
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_READ;
|
|
|
|
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.VIEW;
|
|
import static java.util.stream.Collectors.toList;
|
|
import static java.util.stream.Collectors.toList;
|
|
|
|
|
|
import com.provectus.kafka.ui.api.TopicsApi;
|
|
import com.provectus.kafka.ui.api.TopicsApi;
|
|
@@ -19,8 +24,10 @@ import com.provectus.kafka.ui.model.TopicDTO;
|
|
import com.provectus.kafka.ui.model.TopicDetailsDTO;
|
|
import com.provectus.kafka.ui.model.TopicDetailsDTO;
|
|
import com.provectus.kafka.ui.model.TopicUpdateDTO;
|
|
import com.provectus.kafka.ui.model.TopicUpdateDTO;
|
|
import com.provectus.kafka.ui.model.TopicsResponseDTO;
|
|
import com.provectus.kafka.ui.model.TopicsResponseDTO;
|
|
|
|
+import com.provectus.kafka.ui.model.rbac.AccessContext;
|
|
import com.provectus.kafka.ui.service.TopicsService;
|
|
import com.provectus.kafka.ui.service.TopicsService;
|
|
import com.provectus.kafka.ui.service.analyze.TopicAnalysisService;
|
|
import com.provectus.kafka.ui.service.analyze.TopicAnalysisService;
|
|
|
|
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
|
|
import java.util.Comparator;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
import java.util.List;
|
|
import javax.validation.Valid;
|
|
import javax.validation.Valid;
|
|
@@ -44,66 +51,121 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|
private final TopicsService topicsService;
|
|
private final TopicsService topicsService;
|
|
private final TopicAnalysisService topicAnalysisService;
|
|
private final TopicAnalysisService topicAnalysisService;
|
|
private final ClusterMapper clusterMapper;
|
|
private final ClusterMapper clusterMapper;
|
|
|
|
+ private final AccessControlService accessControlService;
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<TopicDTO>> createTopic(
|
|
public Mono<ResponseEntity<TopicDTO>> createTopic(
|
|
String clusterName, @Valid Mono<TopicCreationDTO> topicCreation, ServerWebExchange exchange) {
|
|
String clusterName, @Valid Mono<TopicCreationDTO> topicCreation, ServerWebExchange exchange) {
|
|
- return topicsService.createTopic(getCluster(clusterName), topicCreation)
|
|
|
|
- .map(clusterMapper::toTopic)
|
|
|
|
- .map(s -> new ResponseEntity<>(s, HttpStatus.OK))
|
|
|
|
- .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topicActions(CREATE)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ topicsService.createTopic(getCluster(clusterName), topicCreation)
|
|
|
|
+ .map(clusterMapper::toTopic)
|
|
|
|
+ .map(s -> new ResponseEntity<>(s, HttpStatus.OK))
|
|
|
|
+ .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()))
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<TopicDTO>> recreateTopic(String clusterName,
|
|
public Mono<ResponseEntity<TopicDTO>> recreateTopic(String clusterName,
|
|
- String topicName, ServerWebExchange serverWebExchange) {
|
|
|
|
- return topicsService.recreateTopic(getCluster(clusterName), topicName)
|
|
|
|
- .map(clusterMapper::toTopic)
|
|
|
|
- .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED));
|
|
|
|
|
|
+ String topicName, ServerWebExchange exchange) {
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(VIEW, CREATE, DELETE)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ topicsService.recreateTopic(getCluster(clusterName), topicName)
|
|
|
|
+ .map(clusterMapper::toTopic)
|
|
|
|
+ .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED))
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<TopicDTO>> cloneTopic(
|
|
public Mono<ResponseEntity<TopicDTO>> cloneTopic(
|
|
String clusterName, String topicName, String newTopicName, ServerWebExchange exchange) {
|
|
String clusterName, String topicName, String newTopicName, ServerWebExchange exchange) {
|
|
- return topicsService.cloneTopic(getCluster(clusterName), topicName, newTopicName)
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(VIEW, CREATE)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(topicsService.cloneTopic(getCluster(clusterName), topicName, newTopicName)
|
|
.map(clusterMapper::toTopic)
|
|
.map(clusterMapper::toTopic)
|
|
- .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED));
|
|
|
|
|
|
+ .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED))
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<Void>> deleteTopic(
|
|
public Mono<ResponseEntity<Void>> deleteTopic(
|
|
String clusterName, String topicName, ServerWebExchange exchange) {
|
|
String clusterName, String topicName, ServerWebExchange exchange) {
|
|
- return topicsService.deleteTopic(getCluster(clusterName), topicName).map(ResponseEntity::ok);
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(DELETE)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ topicsService.deleteTopic(getCluster(clusterName), topicName).map(ResponseEntity::ok)
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<Flux<TopicConfigDTO>>> getTopicConfigs(
|
|
public Mono<ResponseEntity<Flux<TopicConfigDTO>>> getTopicConfigs(
|
|
String clusterName, String topicName, ServerWebExchange exchange) {
|
|
String clusterName, String topicName, ServerWebExchange exchange) {
|
|
- return topicsService.getTopicConfigs(getCluster(clusterName), topicName)
|
|
|
|
- .map(lst -> lst.stream()
|
|
|
|
- .map(InternalTopicConfig::from)
|
|
|
|
- .map(clusterMapper::toTopicConfig)
|
|
|
|
- .collect(toList()))
|
|
|
|
- .map(Flux::fromIterable)
|
|
|
|
- .map(ResponseEntity::ok);
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(VIEW)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ topicsService.getTopicConfigs(getCluster(clusterName), topicName)
|
|
|
|
+ .map(lst -> lst.stream()
|
|
|
|
+ .map(InternalTopicConfig::from)
|
|
|
|
+ .map(clusterMapper::toTopicConfig)
|
|
|
|
+ .collect(toList()))
|
|
|
|
+ .map(Flux::fromIterable)
|
|
|
|
+ .map(ResponseEntity::ok)
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<TopicDetailsDTO>> getTopicDetails(
|
|
public Mono<ResponseEntity<TopicDetailsDTO>> getTopicDetails(
|
|
String clusterName, String topicName, ServerWebExchange exchange) {
|
|
String clusterName, String topicName, ServerWebExchange exchange) {
|
|
- return topicsService.getTopicDetails(getCluster(clusterName), topicName)
|
|
|
|
- .map(clusterMapper::toTopicDetails)
|
|
|
|
- .map(ResponseEntity::ok);
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(VIEW)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ topicsService.getTopicDetails(getCluster(clusterName), topicName)
|
|
|
|
+ .map(clusterMapper::toTopicDetails)
|
|
|
|
+ .map(ResponseEntity::ok)
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
- public Mono<ResponseEntity<TopicsResponseDTO>> getTopics(String clusterName, @Valid Integer page,
|
|
|
|
|
|
+ @Override
|
|
|
|
+ public Mono<ResponseEntity<TopicsResponseDTO>> getTopics(String clusterName,
|
|
|
|
+ @Valid Integer page,
|
|
@Valid Integer perPage,
|
|
@Valid Integer perPage,
|
|
@Valid Boolean showInternal,
|
|
@Valid Boolean showInternal,
|
|
@Valid String search,
|
|
@Valid String search,
|
|
@Valid TopicColumnsToSortDTO orderBy,
|
|
@Valid TopicColumnsToSortDTO orderBy,
|
|
@Valid SortOrderDTO sortOrder,
|
|
@Valid SortOrderDTO sortOrder,
|
|
ServerWebExchange exchange) {
|
|
ServerWebExchange exchange) {
|
|
|
|
+
|
|
return topicsService.getTopicsForPagination(getCluster(clusterName))
|
|
return topicsService.getTopicsForPagination(getCluster(clusterName))
|
|
.flatMap(existingTopics -> {
|
|
.flatMap(existingTopics -> {
|
|
int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
|
|
int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
|
|
@@ -115,7 +177,7 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|
|| showInternal != null && showInternal)
|
|
|| showInternal != null && showInternal)
|
|
.filter(topic -> search == null || StringUtils.contains(topic.getName(), search))
|
|
.filter(topic -> search == null || StringUtils.contains(topic.getName(), search))
|
|
.sorted(comparator)
|
|
.sorted(comparator)
|
|
- .collect(toList());
|
|
|
|
|
|
+ .toList();
|
|
var totalPages = (filtered.size() / pageSize)
|
|
var totalPages = (filtered.size() / pageSize)
|
|
+ (filtered.size() % pageSize == 0 ? 0 : 1);
|
|
+ (filtered.size() % pageSize == 0 ? 0 : 1);
|
|
|
|
|
|
@@ -126,42 +188,34 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|
.collect(toList());
|
|
.collect(toList());
|
|
|
|
|
|
return topicsService.loadTopics(getCluster(clusterName), topicsPage)
|
|
return topicsService.loadTopics(getCluster(clusterName), topicsPage)
|
|
|
|
+ .flatMapMany(Flux::fromIterable)
|
|
|
|
+ .filterWhen(dto -> accessControlService.isTopicAccessible(dto, clusterName))
|
|
|
|
+ .collectList()
|
|
.map(topicsToRender ->
|
|
.map(topicsToRender ->
|
|
new TopicsResponseDTO()
|
|
new TopicsResponseDTO()
|
|
.topics(topicsToRender.stream().map(clusterMapper::toTopic).collect(toList()))
|
|
.topics(topicsToRender.stream().map(clusterMapper::toTopic).collect(toList()))
|
|
.pageCount(totalPages));
|
|
.pageCount(totalPages));
|
|
- }).map(ResponseEntity::ok);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private Comparator<InternalTopic> getComparatorForTopic(
|
|
|
|
- TopicColumnsToSortDTO orderBy) {
|
|
|
|
- var defaultComparator = Comparator.comparing(InternalTopic::getName);
|
|
|
|
- if (orderBy == null) {
|
|
|
|
- return defaultComparator;
|
|
|
|
- }
|
|
|
|
- switch (orderBy) {
|
|
|
|
- case TOTAL_PARTITIONS:
|
|
|
|
- return Comparator.comparing(InternalTopic::getPartitionCount);
|
|
|
|
- case OUT_OF_SYNC_REPLICAS:
|
|
|
|
- return Comparator.comparing(t -> t.getReplicas() - t.getInSyncReplicas());
|
|
|
|
- case REPLICATION_FACTOR:
|
|
|
|
- return Comparator.comparing(InternalTopic::getReplicationFactor);
|
|
|
|
- case SIZE:
|
|
|
|
- return Comparator.comparing(InternalTopic::getSegmentSize);
|
|
|
|
- case NAME:
|
|
|
|
- default:
|
|
|
|
- return defaultComparator;
|
|
|
|
- }
|
|
|
|
|
|
+ })
|
|
|
|
+ .map(ResponseEntity::ok);
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<TopicDTO>> updateTopic(
|
|
public Mono<ResponseEntity<TopicDTO>> updateTopic(
|
|
- String clusterId, String topicName, @Valid Mono<TopicUpdateDTO> topicUpdate,
|
|
|
|
|
|
+ String clusterName, String topicName, @Valid Mono<TopicUpdateDTO> topicUpdate,
|
|
ServerWebExchange exchange) {
|
|
ServerWebExchange exchange) {
|
|
- return topicsService
|
|
|
|
- .updateTopic(getCluster(clusterId), topicName, topicUpdate)
|
|
|
|
- .map(clusterMapper::toTopic)
|
|
|
|
- .map(ResponseEntity::ok);
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(VIEW, EDIT)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ topicsService
|
|
|
|
+ .updateTopic(getCluster(clusterName), topicName, topicUpdate)
|
|
|
|
+ .map(clusterMapper::toTopic)
|
|
|
|
+ .map(ResponseEntity::ok)
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -169,9 +223,18 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|
String clusterName, String topicName,
|
|
String clusterName, String topicName,
|
|
Mono<PartitionsIncreaseDTO> partitionsIncrease,
|
|
Mono<PartitionsIncreaseDTO> partitionsIncrease,
|
|
ServerWebExchange exchange) {
|
|
ServerWebExchange exchange) {
|
|
- return partitionsIncrease.flatMap(partitions ->
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(VIEW, EDIT)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ partitionsIncrease.flatMap(partitions ->
|
|
topicsService.increaseTopicPartitions(getCluster(clusterName), topicName, partitions)
|
|
topicsService.increaseTopicPartitions(getCluster(clusterName), topicName, partitions)
|
|
- ).map(ResponseEntity::ok);
|
|
|
|
|
|
+ ).map(ResponseEntity::ok)
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -179,23 +242,48 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|
String clusterName, String topicName,
|
|
String clusterName, String topicName,
|
|
Mono<ReplicationFactorChangeDTO> replicationFactorChange,
|
|
Mono<ReplicationFactorChangeDTO> replicationFactorChange,
|
|
ServerWebExchange exchange) {
|
|
ServerWebExchange exchange) {
|
|
- return replicationFactorChange
|
|
|
|
- .flatMap(rfc ->
|
|
|
|
- topicsService.changeReplicationFactor(getCluster(clusterName), topicName, rfc))
|
|
|
|
- .map(ResponseEntity::ok);
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(VIEW, EDIT)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ replicationFactorChange
|
|
|
|
+ .flatMap(rfc ->
|
|
|
|
+ topicsService.changeReplicationFactor(getCluster(clusterName), topicName, rfc))
|
|
|
|
+ .map(ResponseEntity::ok)
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<Void>> analyzeTopic(String clusterName, String topicName, ServerWebExchange exchange) {
|
|
public Mono<ResponseEntity<Void>> analyzeTopic(String clusterName, String topicName, ServerWebExchange exchange) {
|
|
- return topicAnalysisService.analyze(getCluster(clusterName), topicName)
|
|
|
|
- .thenReturn(ResponseEntity.ok().build());
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(MESSAGES_READ)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.then(
|
|
|
|
+ topicAnalysisService.analyze(getCluster(clusterName), topicName)
|
|
|
|
+ .thenReturn(ResponseEntity.ok().build())
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
@Override
|
|
public Mono<ResponseEntity<Void>> cancelTopicAnalysis(String clusterName, String topicName,
|
|
public Mono<ResponseEntity<Void>> cancelTopicAnalysis(String clusterName, String topicName,
|
|
- ServerWebExchange exchange) {
|
|
|
|
|
|
+ ServerWebExchange exchange) {
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(MESSAGES_READ)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
topicAnalysisService.cancelAnalysis(getCluster(clusterName), topicName);
|
|
topicAnalysisService.cancelAnalysis(getCluster(clusterName), topicName);
|
|
- return Mono.just(ResponseEntity.ok().build());
|
|
|
|
|
|
+
|
|
|
|
+ return validateAccess.thenReturn(ResponseEntity.ok().build());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -203,10 +291,36 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|
public Mono<ResponseEntity<TopicAnalysisDTO>> getTopicAnalysis(String clusterName,
|
|
public Mono<ResponseEntity<TopicAnalysisDTO>> getTopicAnalysis(String clusterName,
|
|
String topicName,
|
|
String topicName,
|
|
ServerWebExchange exchange) {
|
|
ServerWebExchange exchange) {
|
|
- return Mono.just(
|
|
|
|
- topicAnalysisService.getTopicAnalysis(getCluster(clusterName), topicName)
|
|
|
|
- .map(ResponseEntity::ok)
|
|
|
|
- .orElseGet(() -> ResponseEntity.notFound().build())
|
|
|
|
- );
|
|
|
|
|
|
+
|
|
|
|
+ Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
|
|
|
+ .cluster(clusterName)
|
|
|
|
+ .topic(topicName)
|
|
|
|
+ .topicActions(MESSAGES_READ)
|
|
|
|
+ .build());
|
|
|
|
+
|
|
|
|
+ return validateAccess.thenReturn(topicAnalysisService.getTopicAnalysis(getCluster(clusterName), topicName)
|
|
|
|
+ .map(ResponseEntity::ok)
|
|
|
|
+ .orElseGet(() -> ResponseEntity.notFound().build()));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private Comparator<InternalTopic> getComparatorForTopic(
|
|
|
|
+ TopicColumnsToSortDTO orderBy) {
|
|
|
|
+ var defaultComparator = Comparator.comparing(InternalTopic::getName);
|
|
|
|
+ if (orderBy == null) {
|
|
|
|
+ return defaultComparator;
|
|
|
|
+ }
|
|
|
|
+ switch (orderBy) {
|
|
|
|
+ case TOTAL_PARTITIONS:
|
|
|
|
+ return Comparator.comparing(InternalTopic::getPartitionCount);
|
|
|
|
+ case OUT_OF_SYNC_REPLICAS:
|
|
|
|
+ return Comparator.comparing(t -> t.getReplicas() - t.getInSyncReplicas());
|
|
|
|
+ case REPLICATION_FACTOR:
|
|
|
|
+ return Comparator.comparing(InternalTopic::getReplicationFactor);
|
|
|
|
+ case SIZE:
|
|
|
|
+ return Comparator.comparing(InternalTopic::getSegmentSize);
|
|
|
|
+ case NAME:
|
|
|
|
+ default:
|
|
|
|
+ return defaultComparator;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|