From 1eb82c0c82cdac6ed9aaeadf21ec25ebda5fe4d3 Mon Sep 17 00:00:00 2001 From: Anders Lindh Olsson Date: Thu, 2 Apr 2026 10:17:24 +0200 Subject: [PATCH 1/6] chore(gitignore): add .cursor/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 29d32cd5..f53292f1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.swp *.swo *~ +.cursor/ ### Logs ### *.log From 642e86bae3abe0560c9ec1f614e2bb5a0c7ee2e8 Mon Sep 17 00:00:00 2001 From: Anders Lindh Olsson Date: Thu, 2 Apr 2026 10:38:25 +0200 Subject: [PATCH 2/6] refactor(api): externalize legacy service-root as configurable property - Add LegacyApiProperties @ConfigurationProperties bean for channelfinder.legacy.service-root, normalizing leading/trailing slashes; defaults to ChannelFinder. - Remove hardcoded CFResourceDescriptors constants from all six API interface @RequestMapping declarations; interfaces now carry only HTTP method contracts. - Inject scroll resource URI via @Value in ChannelRepository to avoid static constant coupling. - Document the property in application.properties. --- .../configuration/LegacyApiProperties.java | 47 +++++++++++++++ .../repository/ChannelRepository.java | 58 +++++++++++++++---- .../channelfinder/rest/api/IChannel.java | 19 ------ .../rest/api/IChannelProcessor.java | 3 - .../rest/api/IChannelScroll.java | 4 -- .../phoebus/channelfinder/rest/api/IInfo.java | 4 -- .../channelfinder/rest/api/IProperty.java | 21 ------- .../phoebus/channelfinder/rest/api/ITag.java | 37 ------------ src/main/resources/application.properties | 6 ++ 9 files changed, 101 insertions(+), 98 deletions(-) create mode 100644 src/main/java/org/phoebus/channelfinder/configuration/LegacyApiProperties.java diff --git a/src/main/java/org/phoebus/channelfinder/configuration/LegacyApiProperties.java b/src/main/java/org/phoebus/channelfinder/configuration/LegacyApiProperties.java new file mode 100644 index 00000000..1c46aa81 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/configuration/LegacyApiProperties.java @@ -0,0 +1,47 @@ +package org.phoebus.channelfinder.configuration; + +import jakarta.annotation.PostConstruct; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Configuration for the legacy ChannelFinder HTTP API path prefix. + * + *

The property {@code channelfinder.legacy.service-root} controls the first path segment of all + * legacy-API URLs (e.g. {@code ChannelFinder/resources/channels}). Multi-service deployments + * typically use different roots per service to distinguish paths at a shared reverse proxy. + * + *

The value is normalized on startup: leading and trailing slashes are stripped, and an empty or + * blank value reverts to the default {@code ChannelFinder}. + */ +@Component +@ConfigurationProperties(prefix = "channelfinder.legacy") +public class LegacyApiProperties { + + static final String DEFAULT_ROOT = "ChannelFinder"; + + private String serviceRoot = DEFAULT_ROOT; + + @PostConstruct + void normalize() { + if (serviceRoot == null || serviceRoot.isBlank()) { + serviceRoot = DEFAULT_ROOT; + return; + } + serviceRoot = serviceRoot.strip(); + while (serviceRoot.startsWith("/")) serviceRoot = serviceRoot.substring(1); + while (serviceRoot.endsWith("/")) + serviceRoot = serviceRoot.substring(0, serviceRoot.length() - 1); + if (serviceRoot.isBlank()) { + serviceRoot = DEFAULT_ROOT; + } + } + + public String getServiceRoot() { + return serviceRoot; + } + + public void setServiceRoot(String serviceRoot) { + this.serviceRoot = serviceRoot; + } +} diff --git a/src/main/java/org/phoebus/channelfinder/repository/ChannelRepository.java b/src/main/java/org/phoebus/channelfinder/repository/ChannelRepository.java index 10a4877d..b3b012b0 100644 --- a/src/main/java/org/phoebus/channelfinder/repository/ChannelRepository.java +++ b/src/main/java/org/phoebus/channelfinder/repository/ChannelRepository.java @@ -49,14 +49,14 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import org.phoebus.channelfinder.common.CFResourceDescriptors; import org.phoebus.channelfinder.common.TextUtil; import org.phoebus.channelfinder.configuration.ElasticConfig; +import org.phoebus.channelfinder.configuration.LegacyApiProperties; import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Property; +import org.phoebus.channelfinder.entity.Scroll; import org.phoebus.channelfinder.entity.SearchResult; import org.phoebus.channelfinder.entity.Tag; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -73,15 +73,22 @@ public class ChannelRepository implements CrudRepository { private static final Logger logger = Logger.getLogger(ChannelRepository.class.getName()); - @Autowired ElasticConfig esService; - - @Autowired - @Qualifier("indexClient") - ElasticsearchClient client; + private final ElasticConfig esService; + private final ElasticsearchClient client; + private final String scrollResourceUri; @Value("${repository.chunk.size:10000}") private int chunkSize; + public ChannelRepository( + ElasticConfig esService, + @Qualifier("indexClient") ElasticsearchClient client, + LegacyApiProperties legacyApiProperties) { + this.esService = esService; + this.client = client; + this.scrollResourceUri = legacyApiProperties.getServiceRoot() + "/resources/scroll"; + } + // Object mapper to ignore properties we don't want to index final ObjectMapper objectMapper = new ObjectMapper() @@ -517,9 +524,7 @@ public SearchResult search(MultiValueMap searchParameters) { MessageFormat.format( TextUtil.SEARCH_FAILED_CAUSE, searchParameters, - "Max search window exceeded, use the " - + CFResourceDescriptors.SCROLL_RESOURCE_URI - + " api."); + "Max search window exceeded, use the " + scrollResourceUri + " api."); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); } @@ -764,6 +769,39 @@ public long countByTag(String tagName) { return this.count(params); } + /** + * Scroll-paginated search. Returns a page of channels and the ID to pass as scrollId on the next + * call to advance the cursor. Accepts the same query parameters as {@link #search}. + * + * @param scrollId opaque cursor from a previous call, or {@code null} for the first page + * @param searchParameters channel search parameters + * @return next page with its cursor + */ + public Scroll scroll(String scrollId, MultiValueMap searchParameters) { + BuiltQuery builtQuery = getBuiltQuery(searchParameters); + try { + SearchRequest.Builder builder = + new SearchRequest.Builder() + .index(esService.getES_CHANNEL_INDEX()) + .query(builtQuery.boolQuery.build()._toQuery()) + .from(builtQuery.from) + .size(builtQuery.size) + .sort(SortOptions.of(o -> o.field(FieldSort.of(f -> f.field("name"))))); + if (scrollId != null && !scrollId.isEmpty()) { + builder.searchAfter(FieldValue.of(scrollId)); + } + SearchResponse response = client.search(builder.build(), Channel.class); + List> hits = response.hits().hits(); + return new Scroll( + !hits.isEmpty() ? hits.get(hits.size() - 1).id() : null, + hits.stream().map(Hit::source).toList()); + } catch (IOException | ElasticsearchException e) { + String message = + MessageFormat.format(TextUtil.SEARCH_FAILED_CAUSE, searchParameters, e.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, message, e); + } + } + @Override public void deleteAllById(Iterable ids) { // TODO Auto-generated method stub diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IChannel.java b/src/main/java/org/phoebus/channelfinder/rest/api/IChannel.java index 9dad009e..d947d182 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IChannel.java +++ b/src/main/java/org/phoebus/channelfinder/rest/api/IChannel.java @@ -1,6 +1,5 @@ package org.phoebus.channelfinder.rest.api; -import static org.phoebus.channelfinder.common.CFResourceDescriptors.CHANNEL_RESOURCE_URI; import static org.phoebus.channelfinder.common.CFResourceDescriptors.SEARCH_PARAM_DESCRIPTION; import io.swagger.v3.oas.annotations.Operation; @@ -20,11 +19,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; -@RequestMapping(CHANNEL_RESOURCE_URI) public interface IChannel { @Operation( @@ -272,20 +269,4 @@ long queryCount( }) @DeleteMapping("/{channelName}") void remove(@PathVariable("channelName") String channelName); - - /** - * Checks if 1. the channel name is not null and matches the name in the body 2. the channel owner - * is not null or empty 3. all the listed tags/props exist and prop value is not null or empty - * - * @param channel channel to be validated - */ - void validateChannelRequest(Channel channel); - - /** - * Checks if 1. the tag names are not null 2. the tag owners are not null or empty 3. all the - * channels exist - * - * @param channels list of channels to be validated - */ - void validateChannelRequest(Iterable channels); } diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelProcessor.java b/src/main/java/org/phoebus/channelfinder/rest/api/IChannelProcessor.java index 4c25dcf1..bde4ea2d 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelProcessor.java +++ b/src/main/java/org/phoebus/channelfinder/rest/api/IChannelProcessor.java @@ -1,6 +1,5 @@ package org.phoebus.channelfinder.rest.api; -import static org.phoebus.channelfinder.common.CFResourceDescriptors.CHANNEL_PROCESSOR_RESOURCE_URI; import static org.phoebus.channelfinder.common.CFResourceDescriptors.SEARCH_PARAM_DESCRIPTION; import io.swagger.v3.oas.annotations.Operation; @@ -17,11 +16,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; -@RequestMapping(CHANNEL_PROCESSOR_RESOURCE_URI) public interface IChannelProcessor { @Operation( diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java b/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java index 875d01cd..88fa491e 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java +++ b/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java @@ -1,7 +1,5 @@ package org.phoebus.channelfinder.rest.api; -import static org.phoebus.channelfinder.common.CFResourceDescriptors.SCROLL_RESOURCE_URI; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -13,11 +11,9 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; -@RequestMapping(SCROLL_RESOURCE_URI) public interface IChannelScroll { @Operation( diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IInfo.java b/src/main/java/org/phoebus/channelfinder/rest/api/IInfo.java index 7c8acf0c..8d19310a 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IInfo.java +++ b/src/main/java/org/phoebus/channelfinder/rest/api/IInfo.java @@ -1,16 +1,12 @@ package org.phoebus.channelfinder.rest.api; -import static org.phoebus.channelfinder.common.CFResourceDescriptors.CF_SERVICE_INFO; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -@RequestMapping(CF_SERVICE_INFO) public interface IInfo { @Operation( diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IProperty.java b/src/main/java/org/phoebus/channelfinder/rest/api/IProperty.java index a55b188a..f05a37e4 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IProperty.java +++ b/src/main/java/org/phoebus/channelfinder/rest/api/IProperty.java @@ -1,7 +1,5 @@ package org.phoebus.channelfinder.rest.api; -import static org.phoebus.channelfinder.common.CFResourceDescriptors.PROPERTY_RESOURCE_URI; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -15,11 +13,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; -@RequestMapping(PROPERTY_RESOURCE_URI) public interface IProperty { @Operation( @@ -272,21 +268,4 @@ void removeSingle( * * @param property validate property */ - void validatePropertyRequest(Property property); - - /** - * Checks if 1. the property name is not null and matches the name in the body 2. the property - * owner is not null or empty 3. the property value is not null or empty 4. all the listed - * channels exist - * - * @param properties properties to be validated - */ - void validatePropertyRequest(Iterable properties); - - /** - * Checks if the channel exists - * - * @param channelName check channel exists - */ - void validatePropertyRequest(String channelName); } diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/ITag.java b/src/main/java/org/phoebus/channelfinder/rest/api/ITag.java index 570ccb35..a525f1c3 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/ITag.java +++ b/src/main/java/org/phoebus/channelfinder/rest/api/ITag.java @@ -1,7 +1,5 @@ package org.phoebus.channelfinder.rest.api; -import static org.phoebus.channelfinder.common.CFResourceDescriptors.TAG_RESOURCE_URI; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -15,11 +13,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; -@RequestMapping(TAG_RESOURCE_URI) public interface ITag { @Operation( @@ -277,37 +273,4 @@ Tag addSingle( @DeleteMapping("/{tagName}/{channelName}") void removeSingle( @PathVariable("tagName") String tagName, @PathVariable("channelName") String channelName); - - /** - * Checks if all the tags included satisfy the following conditions - * - *

    - *
  1. the tag names are not null or empty and matches the names in the bodies - *
  2. the tag owners are not null or empty - *
  3. all the channels exist - *
- * - * @param tags the list of tags to be validated - */ - void validateTagRequest(Iterable tags); - - /** - * Checks if tag satisfies the following conditions - * - *
    - *
  1. the tag name is not null or empty and matches the name in the body - *
  2. the tag owner is not null or empty - *
  3. all the listed channels exist - *
- * - * @param tag the tag to be validates - */ - void validateTagRequest(Tag tag); - - /** - * Checks if channel with name "channelName" exists - * - * @param channelName check channel exists - */ - void validateTagWithChannelRequest(String channelName); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d4db1293..b78d2ba0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -111,6 +111,12 @@ elasticsearch.create.indices=true # Repository chunk size, how many channels to submit to elastic at once repository.chunk.size = 10000 +############################## Legacy API path ############################### +# First path segment shared by all legacy API endpoints (e.g. ChannelFinder/resources/channels). +# Change this for multi-service deployments to distinguish services at a shared reverse proxy. +# Leading and trailing slashes are stripped automatically. Default: ChannelFinder +channelfinder.legacy.service-root=ChannelFinder + ############################## Service Info ############################### # ChannelFinder version as defined in the pom file channelfinder.version=@project.version@ From 3350d97f9c665a11b7b3dbb4ed540dee97e81eff Mon Sep 17 00:00:00 2001 From: Anders Lindh Olsson Date: Thu, 2 Apr 2026 10:38:38 +0200 Subject: [PATCH 3/6] refactor(layers): extract service layer, slim controllers to HTTP delegation All business logic (authorization, validation, orchestration) moves out of controllers into dedicated application services. Controllers now contain only constructor injection and single-line delegation calls. - Add ChannelService, TagService, PropertyService, InfoService, ChannelScrollService. - Move processAllChannels scroll orchestration and CF_ADMIN auth check into ChannelProcessorService; remove IChannelScroll dependency from its controller. - Add seven domain exception types (ChannelNotFoundException, TagNotFoundException, PropertyNotFoundException, *ValidationException, UnauthorizedException). - Add ChannelFinderExceptionHandler (@RestControllerAdvice) mapping domain exceptions to HTTP status codes. - Remove validateChannelRequest/validateTagRequest/validatePropertyRequest from API interfaces; validation now lives entirely in the service layer. - Switch all six controllers to constructor injection. - Update ChannelProcessorServiceTest to match the expanded constructor signature. --- .../exceptions/ChannelNotFoundException.java | 8 + .../ChannelValidationException.java | 8 + .../exceptions/PropertyNotFoundException.java | 8 + .../PropertyValidationException.java | 8 + .../exceptions/TagNotFoundException.java | 8 + .../exceptions/TagValidationException.java | 8 + .../exceptions/UnauthorizedException.java | 8 + .../rest/api/IChannelScroll.java | 16 - .../rest/controller/ChannelController.java | 479 +--------------- .../ChannelFinderExceptionHandler.java | 76 +++ .../ChannelProcessorController.java | 55 +- .../controller/ChannelScrollController.java | 182 +----- .../rest/controller/InfoController.java | 57 +- .../rest/controller/PropertyController.java | 536 +----------------- .../rest/controller/TagController.java | 501 +--------------- .../service/ChannelProcessorService.java | 44 +- .../service/ChannelScrollService.java | 20 + .../channelfinder/service/ChannelService.java | 290 ++++++++++ .../channelfinder/service/InfoService.java | 58 ++ .../service/PropertyService.java | 341 +++++++++++ .../channelfinder/service/TagService.java | 275 +++++++++ .../service/external/ArchiverService.java | 5 +- .../ChannelScrollControllerIT.java | 32 +- .../channelfinder/ChannelValidationIT.java | 199 ------- .../channelfinder/PropertyValidationIT.java | 211 ------- .../channelfinder/TagValidationIT.java | 132 ----- .../ChannelProcessorServiceTest.java | 2 +- .../service/ChannelServiceTest.java | 141 +++++ .../service/PropertyServiceTest.java | 140 +++++ .../channelfinder/service/TagServiceTest.java | 99 ++++ 30 files changed, 1633 insertions(+), 2314 deletions(-) create mode 100644 src/main/java/org/phoebus/channelfinder/exceptions/ChannelNotFoundException.java create mode 100644 src/main/java/org/phoebus/channelfinder/exceptions/ChannelValidationException.java create mode 100644 src/main/java/org/phoebus/channelfinder/exceptions/PropertyNotFoundException.java create mode 100644 src/main/java/org/phoebus/channelfinder/exceptions/PropertyValidationException.java create mode 100644 src/main/java/org/phoebus/channelfinder/exceptions/TagNotFoundException.java create mode 100644 src/main/java/org/phoebus/channelfinder/exceptions/TagValidationException.java create mode 100644 src/main/java/org/phoebus/channelfinder/exceptions/UnauthorizedException.java create mode 100644 src/main/java/org/phoebus/channelfinder/rest/controller/ChannelFinderExceptionHandler.java create mode 100644 src/main/java/org/phoebus/channelfinder/service/ChannelScrollService.java create mode 100644 src/main/java/org/phoebus/channelfinder/service/ChannelService.java create mode 100644 src/main/java/org/phoebus/channelfinder/service/InfoService.java create mode 100644 src/main/java/org/phoebus/channelfinder/service/PropertyService.java create mode 100644 src/main/java/org/phoebus/channelfinder/service/TagService.java delete mode 100644 src/test/java/org/phoebus/channelfinder/ChannelValidationIT.java delete mode 100644 src/test/java/org/phoebus/channelfinder/PropertyValidationIT.java delete mode 100644 src/test/java/org/phoebus/channelfinder/TagValidationIT.java create mode 100644 src/test/java/org/phoebus/channelfinder/service/ChannelServiceTest.java create mode 100644 src/test/java/org/phoebus/channelfinder/service/PropertyServiceTest.java create mode 100644 src/test/java/org/phoebus/channelfinder/service/TagServiceTest.java diff --git a/src/main/java/org/phoebus/channelfinder/exceptions/ChannelNotFoundException.java b/src/main/java/org/phoebus/channelfinder/exceptions/ChannelNotFoundException.java new file mode 100644 index 00000000..789c93e5 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/exceptions/ChannelNotFoundException.java @@ -0,0 +1,8 @@ +package org.phoebus.channelfinder.exceptions; + +public class ChannelNotFoundException extends RuntimeException { + + public ChannelNotFoundException(String channelName) { + super("Channel not found: " + channelName); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/exceptions/ChannelValidationException.java b/src/main/java/org/phoebus/channelfinder/exceptions/ChannelValidationException.java new file mode 100644 index 00000000..21728686 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/exceptions/ChannelValidationException.java @@ -0,0 +1,8 @@ +package org.phoebus.channelfinder.exceptions; + +public class ChannelValidationException extends RuntimeException { + + public ChannelValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/exceptions/PropertyNotFoundException.java b/src/main/java/org/phoebus/channelfinder/exceptions/PropertyNotFoundException.java new file mode 100644 index 00000000..457c5a43 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/exceptions/PropertyNotFoundException.java @@ -0,0 +1,8 @@ +package org.phoebus.channelfinder.exceptions; + +public class PropertyNotFoundException extends RuntimeException { + + public PropertyNotFoundException(String propertyName) { + super("Property not found: " + propertyName); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/exceptions/PropertyValidationException.java b/src/main/java/org/phoebus/channelfinder/exceptions/PropertyValidationException.java new file mode 100644 index 00000000..5f288635 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/exceptions/PropertyValidationException.java @@ -0,0 +1,8 @@ +package org.phoebus.channelfinder.exceptions; + +public class PropertyValidationException extends RuntimeException { + + public PropertyValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/exceptions/TagNotFoundException.java b/src/main/java/org/phoebus/channelfinder/exceptions/TagNotFoundException.java new file mode 100644 index 00000000..85af9ae5 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/exceptions/TagNotFoundException.java @@ -0,0 +1,8 @@ +package org.phoebus.channelfinder.exceptions; + +public class TagNotFoundException extends RuntimeException { + + public TagNotFoundException(String tagName) { + super("Tag not found: " + tagName); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/exceptions/TagValidationException.java b/src/main/java/org/phoebus/channelfinder/exceptions/TagValidationException.java new file mode 100644 index 00000000..01089bd6 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/exceptions/TagValidationException.java @@ -0,0 +1,8 @@ +package org.phoebus.channelfinder.exceptions; + +public class TagValidationException extends RuntimeException { + + public TagValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/exceptions/UnauthorizedException.java b/src/main/java/org/phoebus/channelfinder/exceptions/UnauthorizedException.java new file mode 100644 index 00000000..15d9ace6 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/exceptions/UnauthorizedException.java @@ -0,0 +1,8 @@ +package org.phoebus.channelfinder.exceptions; + +public class UnauthorizedException extends RuntimeException { + + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java b/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java index 88fa491e..c98b9ed1 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java +++ b/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java @@ -60,20 +60,4 @@ Scroll query( String scrollId, @Parameter(description = CFResourceDescriptors.SEARCH_PARAM_DESCRIPTION) @RequestParam MultiValueMap searchParameters); - - /** - * Search for a list of channels based on their name, tags, and/or properties. Search parameters - * ~name - The name of the channel ~tags - A list of comma separated values - * ${propertyName}:${propertyValue} - - * - *

The query result is sorted based on the channel name ~size - The number of channels to be - * returned ~from - The starting index of the channel list - * - *

TODO combine with ChannelRepository code. - * - * @param scrollId scroll ID - * @param searchParameters - search parameters for scrolling searches - * @return search scroll - */ - Scroll search(String scrollId, MultiValueMap searchParameters); } diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelController.java b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelController.java index 57c3267f..03563c8d 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelController.java +++ b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelController.java @@ -1,517 +1,70 @@ package org.phoebus.channelfinder.rest.controller; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.Lists; -import jakarta.servlet.ServletContext; -import java.text.MessageFormat; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; -import org.phoebus.channelfinder.common.TextUtil; import org.phoebus.channelfinder.entity.Channel; -import org.phoebus.channelfinder.entity.Property; import org.phoebus.channelfinder.entity.SearchResult; -import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.repository.ChannelRepository; -import org.phoebus.channelfinder.repository.PropertyRepository; -import org.phoebus.channelfinder.repository.TagRepository; import org.phoebus.channelfinder.rest.api.IChannel; -import org.phoebus.channelfinder.service.AuthorizationService; -import org.phoebus.channelfinder.service.AuthorizationService.ROLES; -import org.phoebus.channelfinder.service.ChannelProcessorService; -import org.springframework.beans.factory.annotation.Autowired; +import org.phoebus.channelfinder.service.ChannelService; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; @CrossOrigin @RestController @EnableAutoConfiguration +@RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/channels") public class ChannelController implements IChannel { - private static final Logger channelManagerAudit = - Logger.getLogger(ChannelController.class.getName() + ".audit"); - private static final Logger logger = Logger.getLogger(ChannelController.class.getName()); + private final ChannelService channelService; - @Autowired private ServletContext servletContext; - - @Autowired TagRepository tagRepository; - - @Autowired PropertyRepository propertyRepository; - - @Autowired ChannelRepository channelRepository; - - @Autowired AuthorizationService authorizationService; - - @Autowired ChannelProcessorService channelProcessorService; + public ChannelController(ChannelService channelService) { + this.channelService = channelService; + } @Override public List query(MultiValueMap allRequestParams) { - return channelRepository.search(allRequestParams).channels(); + return channelService.query(allRequestParams); } @Override public SearchResult combinedQuery(MultiValueMap allRequestParams) { - return channelRepository.search(allRequestParams); + return channelService.combinedQuery(allRequestParams); } @Override public long queryCount(MultiValueMap allRequestParams) { - return channelRepository.count(allRequestParams); + return channelService.queryCount(allRequestParams); } @Override public Channel read(String channelName) { - channelManagerAudit.log( - Level.INFO, () -> MessageFormat.format(TextUtil.FIND_CHANNEL, channelName)); - - Optional foundChannel = channelRepository.findById(channelName); - if (foundChannel.isPresent()) return foundChannel.get(); - else { - String message = MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } + return channelService.read(channelName); } @Override public Channel create(String channelName, Channel channel) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_CHANNEL)) { - channelManagerAudit.log( - Level.INFO, () -> MessageFormat.format(TextUtil.CREATE_CHANNEL, channel.toLog())); - // Validate request parameters - validateChannelRequest(channel); - - // check if authorized owner - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), channel), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - channel, - HttpStatus.UNAUTHORIZED); - Optional existingChannel = channelRepository.findById(channelName); - boolean present = existingChannel.isPresent(); - if (present) { - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingChannel.get()), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - existingChannel.get(), - HttpStatus.UNAUTHORIZED); - // delete existing channel - channelRepository.deleteById(channelName); - } - - // reset owners of attached tags/props back to existing owners - channel - .getProperties() - .forEach( - prop -> prop.setOwner(propertyRepository.findById(prop.getName()).get().getOwner())); - channel - .getTags() - .forEach(tag -> tag.setOwner(tagRepository.findById(tag.getName()).get().getOwner())); - - Channel createdChannel = channelRepository.index(channel); - // process the results - channelProcessorService.sendToProcessors(List.of(createdChannel)); - // create new channel - return createdChannel; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return channelService.create(channelName, channel); } @Override public Iterable create(Iterable channels) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_CHANNEL)) { - // check if authorized owner - long start = System.currentTimeMillis(); - Map existingChannels = - channelRepository - .findAllById( - StreamSupport.stream(channels.spliterator(), true).map(Channel::getName).toList()) - .stream() - .collect(Collectors.toMap(Channel::getName, channel -> channel)); - for (Channel channel : channels) { - boolean present = existingChannels.containsKey(channel.getName()); - if (present) { - Channel existingChannel = existingChannels.get(channel.getName()); - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingChannel), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - existingChannel, - HttpStatus.UNAUTHORIZED); - channel.setOwner(existingChannel.getOwner()); - } else { - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), channel), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - channel, - HttpStatus.UNAUTHORIZED); - } - } - logger.log( - Level.INFO, - "Completed Authorization check : " + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - // Validate request parameters - validateChannelRequest(channels); - logger.log( - Level.INFO, - "Completed validation check : " + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - - // delete existing channels - channelRepository.deleteAll(channels); - logger.log( - Level.INFO, - "Completed replacement of Channels : " + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - - // reset owners of attached tags/props back to existing owners - resetOwnersToExisting(channels); - - logger.log( - Level.INFO, - "Completed reset tag and property ownership : " - + (System.currentTimeMillis() - start) - + "ms"); - start = System.currentTimeMillis(); - - logger.log(Level.INFO, "Completed logging : " + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - List createdChannels = channelRepository.indexAll(Lists.newArrayList(channels)); - - logger.log(Level.INFO, "Completed indexing : " + (System.currentTimeMillis() - start) + "ms"); - // process the results - channelProcessorService.sendToProcessors(createdChannels); - // created new channel - return createdChannels; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNELS, channels); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - - private void resetOwnersToExisting(Iterable channels) { - Map propOwners = - StreamSupport.stream(propertyRepository.findAll().spliterator(), true) - .collect(Collectors.toUnmodifiableMap(Property::getName, Property::getOwner)); - Map tagOwners = - StreamSupport.stream(tagRepository.findAll().spliterator(), true) - .collect(Collectors.toUnmodifiableMap(Tag::getName, Tag::getOwner)); - - for (Channel channel : channels) { - channel.getProperties().forEach(prop -> prop.setOwner(propOwners.get(prop.getName()))); - channel.getTags().forEach(tag -> tag.setOwner(tagOwners.get(tag.getName()))); - } + return channelService.create(channels); } @Override public Channel update(String channelName, Channel channel) { - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_CHANNEL)) { - long start = System.currentTimeMillis(); - - // Validate request parameters - validateChannelRequest(channel); - - final long time = System.currentTimeMillis() - start; - channelManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.PATH_POST_VALIDATION_TIME, servletContext.getContextPath(), time)); - - // check if authorized owner - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), channel), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - channel, - HttpStatus.UNAUTHORIZED); - Optional existingChannel = channelRepository.findById(channelName); - boolean present = existingChannel.isPresent(); - - Channel newChannel; - if (present) { - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingChannel.get()), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - existingChannel.get(), - HttpStatus.UNAUTHORIZED); - newChannel = existingChannel.get(); - newChannel.setOwner(channel.getOwner()); - newChannel.addProperties(channel.getProperties()); - newChannel.addTags(channel.getTags()); - // Is an existing channel being renamed - if (!channel.getName().equalsIgnoreCase(existingChannel.get().getName())) { - // Since this is a rename operation we will need to remove the old channel. - channelRepository.deleteById(existingChannel.get().getName()); - newChannel.setName(channel.getName()); - } - } else { - newChannel = channel; - } - - // reset owners of attached tags/props back to existing owners - channel - .getProperties() - .forEach( - prop -> prop.setOwner(propertyRepository.findById(prop.getName()).get().getOwner())); - channel - .getTags() - .forEach(tag -> tag.setOwner(tagRepository.findById(tag.getName()).get().getOwner())); - - Channel updatedChannels = channelRepository.save(newChannel); - // process the results - channelProcessorService.sendToProcessors(List.of(updatedChannels)); - // created new channel - return updatedChannels; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return channelService.update(channelName, channel); } @Override public Iterable update(Iterable channels) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_CHANNEL)) { - long start = System.currentTimeMillis(); - - for (Channel channel : channels) { - Optional existingChannel = channelRepository.findById(channel.getName()); - boolean present = existingChannel.isPresent(); - if (present) { - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingChannel.get()), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - existingChannel.get(), - HttpStatus.UNAUTHORIZED); - channel.setOwner(existingChannel.get().getOwner()); - } else { - checkAndThrow( - !authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), channel), - TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, - channel, - HttpStatus.UNAUTHORIZED); - } - } - - // Validate request parameters - validateChannelRequest(channels); - - final long time = System.currentTimeMillis() - start; - channelManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.PATH_POST_PREPERATION_TIME, servletContext.getContextPath(), time)); - - // reset owners of attached tags/props back to existing owners - resetOwnersToExisting(channels); - channelManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.PATH_POST_PREPERATION_TIME, servletContext.getContextPath(), time)); - - // update channels - List updatedChannels = - FluentIterable.from(channelRepository.saveAll(channels)).toList(); - // process the results - channelProcessorService.sendToProcessors(updatedChannels); - // created new channel - return updatedChannels; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNELS, channels); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return channelService.update(channels); } @Override public void remove(String channelName) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_CHANNEL)) { - channelManagerAudit.log( - Level.INFO, () -> MessageFormat.format(TextUtil.DELETE_CHANNEL, channelName)); - Optional existingChannel = channelRepository.findById(channelName); - if (existingChannel.isPresent()) { - // check if authorized owner - if (authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingChannel.get())) { - // delete channel - channelRepository.deleteById(channelName); - } else { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } else { - String message = MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - - /** - * Checks if 1. the channel name is not null and matches the name in the body 2. the channel owner - * is not null or empty 3. all the listed tags/props exist and prop value is not null or empty - * - * @param channel channel to be validated - */ - @Override - public void validateChannelRequest(Channel channel) { - // 1 - checkAndThrow( - channel.getName() == null || channel.getName().isEmpty(), - TextUtil.CHANNEL_NAME_CANNOT_BE_NULL_OR_EMPTY, - channel, - HttpStatus.BAD_REQUEST); - // 2 - checkAndThrow( - channel.getOwner() == null || channel.getOwner().isEmpty(), - TextUtil.CHANNEL_OWNER_CANNOT_BE_NULL_OR_EMPTY, - channel, - HttpStatus.BAD_REQUEST); - // 3 - checkTags(channel); - // 3 - checkProperties(channel); - } - - private void checkProperties(Channel channel) { - List propertyNames = channel.getProperties().stream().map(Property::getName).toList(); - List propertyValues = channel.getProperties().stream().map(Property::getValue).toList(); - for (String propertyName : propertyNames) { - if (!propertyRepository.existsById(propertyName)) { - String message = MessageFormat.format(TextUtil.PROPERTY_NAME_DOES_NOT_EXIST, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } - checkValues(propertyNames, propertyValues); - } - - private void checkValues(List propertyNames, List propertyValues) { - for (String propertyValue : propertyValues) { - if (propertyValue == null || propertyValue.isEmpty()) { - String message = - MessageFormat.format( - TextUtil.PROPERTY_VALUE_NULL_OR_EMPTY, - propertyNames.get(propertyValues.indexOf(propertyValue)), - propertyValue); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); - } - } - } - - private void checkTags(Channel channel) { - List tagNames = channel.getTags().stream().map(Tag::getName).toList(); - for (String tagName : tagNames) { - if (!tagRepository.existsById(tagName)) { - String message = MessageFormat.format(TextUtil.TAG_NAME_DOES_NOT_EXIST, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } - } - - private static void checkAndThrow( - boolean channel, - String channelNameCannotBeNullOrEmpty, - Channel channel1, - HttpStatus badRequest) { - if (channel) { - String message = MessageFormat.format(channelNameCannotBeNullOrEmpty, channel1.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(badRequest)); - throw new ResponseStatusException(badRequest, message, null); - } - } - - /** - * Checks if 1. the tag names are not null 2. the tag owners are not null or empty 3. all the - * channels exist - * - * @param channels list of channels to be validated - */ - @Override - public void validateChannelRequest(Iterable channels) { - List existingProperties = - StreamSupport.stream(propertyRepository.findAll().spliterator(), true) - .map(Property::getName) - .toList(); - List existingTags = - StreamSupport.stream(tagRepository.findAll().spliterator(), true) - .map(Tag::getName) - .toList(); - for (Channel channel : channels) { - // 1 - checkAndThrow( - channel.getName() == null || channel.getName().isEmpty(), - TextUtil.CHANNEL_NAME_CANNOT_BE_NULL_OR_EMPTY, - channel, - HttpStatus.BAD_REQUEST); - // 2 - checkAndThrow( - channel.getOwner() == null || channel.getOwner().isEmpty(), - TextUtil.CHANNEL_OWNER_CANNOT_BE_NULL_OR_EMPTY, - channel, - HttpStatus.BAD_REQUEST); - // 3 - List tagNames = channel.getTags().stream().map(Tag::getName).toList(); - for (String tagName : tagNames) { - if (!existingTags.contains(tagName)) { - String message = MessageFormat.format(TextUtil.TAG_NAME_DOES_NOT_EXIST, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } - // 3 - List propertyNames = channel.getProperties().stream().map(Property::getName).toList(); - List propertyValues = - channel.getProperties().stream().map(Property::getValue).toList(); - for (String propertyName : propertyNames) { - if (!existingProperties.contains(propertyName)) { - String message = - MessageFormat.format(TextUtil.PROPERTY_NAME_DOES_NOT_EXIST, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } - checkValues(propertyNames, propertyValues); - } + channelService.remove(channelName); } } diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelFinderExceptionHandler.java b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelFinderExceptionHandler.java new file mode 100644 index 00000000..ab846891 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelFinderExceptionHandler.java @@ -0,0 +1,76 @@ +package org.phoebus.channelfinder.rest.controller; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.phoebus.channelfinder.exceptions.ChannelNotFoundException; +import org.phoebus.channelfinder.exceptions.ChannelValidationException; +import org.phoebus.channelfinder.exceptions.PropertyNotFoundException; +import org.phoebus.channelfinder.exceptions.PropertyValidationException; +import org.phoebus.channelfinder.exceptions.TagNotFoundException; +import org.phoebus.channelfinder.exceptions.TagValidationException; +import org.phoebus.channelfinder.exceptions.UnauthorizedException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; + +/** + * Translates domain exceptions thrown by service classes to HTTP responses. Controllers should + * propagate domain exceptions; this advice maps them to status codes in one place. + */ +@RestControllerAdvice +public class ChannelFinderExceptionHandler { + + private static final Logger logger = + Logger.getLogger(ChannelFinderExceptionHandler.class.getName()); + + @ExceptionHandler(ChannelNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseStatusException handleChannelNotFound(ChannelNotFoundException ex) { + logger.log(Level.SEVERE, ex.getMessage()); + return new ResponseStatusException(HttpStatus.NOT_FOUND, ex.getMessage()); + } + + @ExceptionHandler(TagNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseStatusException handleTagNotFound(TagNotFoundException ex) { + logger.log(Level.SEVERE, ex.getMessage()); + return new ResponseStatusException(HttpStatus.NOT_FOUND, ex.getMessage()); + } + + @ExceptionHandler(PropertyNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseStatusException handlePropertyNotFound(PropertyNotFoundException ex) { + logger.log(Level.SEVERE, ex.getMessage()); + return new ResponseStatusException(HttpStatus.NOT_FOUND, ex.getMessage()); + } + + @ExceptionHandler(ChannelValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseStatusException handleChannelValidation(ChannelValidationException ex) { + logger.log(Level.SEVERE, ex.getMessage()); + return new ResponseStatusException(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(TagValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseStatusException handleTagValidation(TagValidationException ex) { + logger.log(Level.SEVERE, ex.getMessage()); + return new ResponseStatusException(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(PropertyValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseStatusException handlePropertyValidation(PropertyValidationException ex) { + logger.log(Level.SEVERE, ex.getMessage()); + return new ResponseStatusException(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(UnauthorizedException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ResponseStatusException handleUnauthorized(UnauthorizedException ex) { + logger.log(Level.SEVERE, ex.getMessage()); + return new ResponseStatusException(HttpStatus.UNAUTHORIZED, ex.getMessage()); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelProcessorController.java b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelProcessorController.java index 201b2108..1e40e47e 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelProcessorController.java +++ b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelProcessorController.java @@ -1,39 +1,25 @@ package org.phoebus.channelfinder.rest.controller; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; import org.phoebus.channelfinder.entity.Channel; -import org.phoebus.channelfinder.entity.Scroll; import org.phoebus.channelfinder.rest.api.IChannelProcessor; -import org.phoebus.channelfinder.rest.api.IChannelScroll; -import org.phoebus.channelfinder.service.AuthorizationService; import org.phoebus.channelfinder.service.ChannelProcessorService; import org.phoebus.channelfinder.service.model.archiver.ChannelProcessorInfo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; @RestController @EnableAutoConfiguration +@RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/processors") public class ChannelProcessorController implements IChannelProcessor { - private static final Logger logger = Logger.getLogger(ChannelProcessorController.class.getName()); + private final ChannelProcessorService channelProcessorService; - @Autowired ChannelProcessorService channelProcessorService; - @Autowired AuthorizationService authorizationService; - - // TODO replace with PIT and search_after - @Autowired IChannelScroll channelScroll; - - @Value("${elasticsearch.query.size:10000}") - private int defaultMaxSize; + public ChannelProcessorController(ChannelProcessorService channelProcessorService) { + this.channelProcessorService = channelProcessorService; + } @Override public long processorCount() { @@ -47,37 +33,12 @@ public List processorInfo() { @Override public long processAllChannels() { - logger.log(Level.INFO, "Calling processor on ALL channels in ChannelFinder"); - // Only allow authorized users to trigger this operation - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), - AuthorizationService.ROLES.CF_ADMIN)) { - MultiValueMap searchParameters = new LinkedMultiValueMap(); - searchParameters.add("~name", "*"); - return processChannels(searchParameters); - } else { - logger.log( - Level.SEVERE, - "User does not have the proper authorization to perform this operation: /process/all", - new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException( - HttpStatus.UNAUTHORIZED, - "User does not have the proper authorization to perform this operation: /process/all"); - } + return channelProcessorService.processAllChannels(); } @Override public long processChannels(MultiValueMap allRequestParams) { - long channelCount = 0; - Scroll scrollResult = channelScroll.query(allRequestParams); - channelCount += scrollResult.getChannels().size(); - processChannels(scrollResult.getChannels()); - while (scrollResult.getChannels().size() == defaultMaxSize) { - scrollResult = channelScroll.search(scrollResult.getId(), allRequestParams); - channelCount += scrollResult.getChannels().size(); - processChannels(scrollResult.getChannels()); - } - return channelCount; + return channelProcessorService.processChannelsByQuery(allRequestParams); } @Override diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelScrollController.java b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelScrollController.java index 893d17bf..7e2fc6e4 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelScrollController.java +++ b/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelScrollController.java @@ -1,193 +1,33 @@ package org.phoebus.channelfinder.rest.controller; -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch._types.FieldSort; -import co.elastic.clients.elasticsearch._types.FieldValue; -import co.elastic.clients.elasticsearch._types.SortOptions; -import co.elastic.clients.elasticsearch._types.query_dsl.*; -import co.elastic.clients.elasticsearch.core.SearchRequest; -import co.elastic.clients.elasticsearch.core.SearchResponse; -import co.elastic.clients.elasticsearch.core.search.Hit; -import java.text.MessageFormat; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import org.phoebus.channelfinder.common.TextUtil; -import org.phoebus.channelfinder.configuration.ElasticConfig; -import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Scroll; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; +import org.phoebus.channelfinder.rest.api.IChannelScroll; +import org.phoebus.channelfinder.service.ChannelScrollService; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.http.HttpStatus; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; @CrossOrigin @RestController @EnableAutoConfiguration -public class ChannelScrollController implements org.phoebus.channelfinder.rest.api.IChannelScroll { +@RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/scroll") +public class ChannelScrollController implements IChannelScroll { - private static final Logger logger = Logger.getLogger(ChannelScrollController.class.getName()); + private final ChannelScrollService channelScrollService; - @Autowired ElasticConfig esService; - - @Autowired - @Qualifier("indexClient") - ElasticsearchClient client; + public ChannelScrollController(ChannelScrollService channelScrollService) { + this.channelScrollService = channelScrollService; + } @Override public Scroll query(MultiValueMap allRequestParams) { - return search(null, allRequestParams); + return channelScrollService.search(null, allRequestParams); } @Override public Scroll query(String scrollId, MultiValueMap searchParameters) { - return search(scrollId, searchParameters); - } - - /** - * Search for a list of channels based on their name, tags, and/or properties. Search parameters - * ~name - The name of the channel ~tags - A list of comma separated values - * ${propertyName}:${propertyValue} - - * - *

The query result is sorted based on the channel name ~size - The number of channels to be - * returned ~from - The starting index of the channel list - * - *

TODO combine with ChannelRepository code. - * - * @param scrollId scroll ID - * @param searchParameters - search parameters for scrolling searches - * @return search scroll - */ - @Override - public Scroll search(String scrollId, MultiValueMap searchParameters) { - BoolQuery.Builder boolQuery = new BoolQuery.Builder(); - int size = esService.getES_QUERY_SIZE(); - int from = 0; - - for (Map.Entry> parameter : searchParameters.entrySet()) { - String key = parameter.getKey().trim(); - boolean isNot = key.endsWith("!"); - if (isNot) { - key = key.substring(0, key.length() - 1); - } - switch (key) { - case "~name": - for (String value : parameter.getValue()) { - DisMaxQuery.Builder nameQuery = new DisMaxQuery.Builder(); - for (String pattern : value.split("[\\|,;]")) { - nameQuery.queries( - WildcardQuery.of(w -> w.field("name").value(pattern.trim()))._toQuery()); - } - boolQuery.must(nameQuery.build()._toQuery()); - } - break; - case "~tag": - for (String value : parameter.getValue()) { - DisMaxQuery.Builder tagQuery = new DisMaxQuery.Builder(); - for (String pattern : value.split("[\\|,;]")) { - tagQuery.queries( - NestedQuery.of( - n -> - n.path("tags") - .query( - WildcardQuery.of( - w -> w.field("tags.name").value(pattern.trim())) - ._toQuery())) - ._toQuery()); - } - if (isNot) { - boolQuery.mustNot(tagQuery.build()._toQuery()); - } else { - boolQuery.must(tagQuery.build()._toQuery()); - } - } - break; - case "~size": - Optional maxSize = - parameter.getValue().stream().max(Comparator.comparing(Integer::valueOf)); - if (maxSize.isPresent()) { - size = Integer.parseInt(maxSize.get()); - } - break; - case "~from": - Optional maxFrom = - parameter.getValue().stream().max(Comparator.comparing(Integer::valueOf)); - if (maxFrom.isPresent()) { - from = Integer.parseInt(maxFrom.get()); - } - break; - - default: - DisMaxQuery.Builder propertyQuery = new DisMaxQuery.Builder(); - for (String value : parameter.getValue()) { - for (String pattern : value.split("[\\|,;]")) { - String finalKey = key; - BoolQuery bq; - if (isNot) { - bq = - BoolQuery.of( - p -> - p.must( - MatchQuery.of( - name -> name.field("properties.name").query(finalKey)) - ._toQuery()) - .mustNot( - WildcardQuery.of( - val -> - val.field("properties.value").value(pattern.trim())) - ._toQuery())); - } else { - bq = - BoolQuery.of( - p -> - p.must( - MatchQuery.of( - name -> name.field("properties.name").query(finalKey)) - ._toQuery()) - .must( - WildcardQuery.of( - val -> - val.field("properties.value").value(pattern.trim())) - ._toQuery())); - } - propertyQuery.queries( - NestedQuery.of(n -> n.path("properties").query(bq._toQuery()))._toQuery()); - } - } - boolQuery.must(propertyQuery.build()._toQuery()); - break; - } - } - - try { - SearchRequest.Builder builder = new SearchRequest.Builder(); - builder - .index(esService.getES_CHANNEL_INDEX()) - .query(boolQuery.build()._toQuery()) - .from(from) - .size(size) - .sort(SortOptions.of(o -> o.field(FieldSort.of(f -> f.field("name"))))); - if (scrollId != null && !scrollId.isEmpty()) { - builder.searchAfter(FieldValue.of(scrollId)); - } - SearchResponse response = client.search(builder.build(), Channel.class); - List> hits = response.hits().hits(); - return new Scroll( - !hits.isEmpty() ? hits.get(hits.size() - 1).id() : null, - hits.stream().map(Hit::source).collect(Collectors.toList())); - } catch (Exception e) { - String message = - MessageFormat.format(TextUtil.SEARCH_FAILED_CAUSE, searchParameters, e.getMessage()); - logger.log(Level.SEVERE, message, e); - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, message, e); - } + return channelScrollService.search(scrollId, searchParameters); } } diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/InfoController.java b/src/main/java/org/phoebus/channelfinder/rest/controller/InfoController.java index 3a0a96cb..89024a43 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/InfoController.java +++ b/src/main/java/org/phoebus/channelfinder/rest/controller/InfoController.java @@ -1,67 +1,26 @@ package org.phoebus.channelfinder.rest.controller; -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch._types.ElasticsearchVersionInfo; -import co.elastic.clients.elasticsearch.core.InfoResponse; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.phoebus.channelfinder.configuration.ElasticConfig; import org.phoebus.channelfinder.rest.api.IInfo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import org.phoebus.channelfinder.service.InfoService; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @CrossOrigin @RestController @EnableAutoConfiguration +@RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}") public class InfoController implements IInfo { - private static final Logger logger = Logger.getLogger(InfoController.class.getName()); + private final InfoService infoService; - @Value("${channelfinder.version:4.7.0}") - private String version; - - @Autowired private ElasticConfig esService; - - private static final ObjectMapper objectMapper = - new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + public InfoController(InfoService infoService) { + this.infoService = infoService; + } @Override public String info() { - - Map cfServiceInfo = new LinkedHashMap<>(); - cfServiceInfo.put("name", "ChannelFinder Service"); - cfServiceInfo.put("version", version); - - Map elasticInfo = new LinkedHashMap<>(); - try { - - ElasticsearchClient client = esService.getSearchClient(); - InfoResponse response = client.info(); - - elasticInfo.put("status", "Connected"); - elasticInfo.put("clusterName", response.clusterName()); - elasticInfo.put("clusterUuid", response.clusterUuid()); - ElasticsearchVersionInfo elasticVersion = response.version(); - elasticInfo.put("version", elasticVersion.number()); - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to create ChannelFinder service info resource.", e); - elasticInfo.put("status", "Failed to connect to elastic " + e.getLocalizedMessage()); - } - cfServiceInfo.put("elastic", elasticInfo); - try { - return objectMapper.writeValueAsString(cfServiceInfo); - } catch (JsonProcessingException e) { - logger.log(Level.WARNING, "Failed to create ChannelFinder service info resource.", e); - return "Failed to gather ChannelFinder service info"; - } + return infoService.info(); } } diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/PropertyController.java b/src/main/java/org/phoebus/channelfinder/rest/controller/PropertyController.java index e6ce6aeb..67f92aa7 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/PropertyController.java +++ b/src/main/java/org/phoebus/channelfinder/rest/controller/PropertyController.java @@ -1,569 +1,67 @@ package org.phoebus.channelfinder.rest.controller; -import com.google.common.collect.Lists; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.phoebus.channelfinder.common.TextUtil; -import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Property; -import org.phoebus.channelfinder.repository.ChannelRepository; -import org.phoebus.channelfinder.repository.PropertyRepository; -import org.phoebus.channelfinder.repository.TagRepository; import org.phoebus.channelfinder.rest.api.IProperty; -import org.phoebus.channelfinder.service.AuthorizationService; -import org.phoebus.channelfinder.service.AuthorizationService.ROLES; -import org.springframework.beans.factory.annotation.Autowired; +import org.phoebus.channelfinder.service.PropertyService; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; @CrossOrigin @RestController @EnableAutoConfiguration +@RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/properties") public class PropertyController implements IProperty { - private static final Logger propertyManagerAudit = - Logger.getLogger(PropertyController.class.getName() + ".audit"); - private static final Logger logger = Logger.getLogger(PropertyController.class.getName()); + private final PropertyService propertyService; - @Autowired TagRepository tagRepository; - - @Autowired PropertyRepository propertyRepository; - - @Autowired ChannelRepository channelRepository; - - @Autowired AuthorizationService authorizationService; + public PropertyController(PropertyService propertyService) { + this.propertyService = propertyService; + } @Override public Iterable list() { - return propertyRepository.findAll(); + return propertyService.list(); } @Override public Property read(String propertyName, boolean withChannels) { - propertyManagerAudit.log( - Level.INFO, () -> MessageFormat.format(TextUtil.FIND_PROPERTY, propertyName)); - - Optional foundProperty; - if (withChannels) { - foundProperty = propertyRepository.findById(propertyName, true); - } else { - foundProperty = propertyRepository.findById(propertyName); - } - if (foundProperty.isPresent()) { - return foundProperty.get(); - } else { - String message = MessageFormat.format(TextUtil.PROPERTY_NAME_DOES_NOT_EXIST, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } + return propertyService.read(propertyName, withChannels); } @Override public Property create(String propertyName, Property property) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_PROPERTY)) { - long start = System.currentTimeMillis(); - propertyManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - // Validate request parameters - validatePropertyRequest(property); - - // check if authorized owner - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), property)) { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, property.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - Optional existingProperty = propertyRepository.findById(propertyName); - boolean present = existingProperty.isPresent(); - if (present) { - checkPropertyAuthorization(existingProperty); - // delete existing property - propertyRepository.deleteById(propertyName); - } - - // create new property - Property createdProperty = propertyRepository.index(property); - - if (!property.getChannels().isEmpty()) { - // update the listed channels in the property's payload with the new property - Iterable chans = channelRepository.saveAll(property.getChannels()); - // TODO validate the above result - List chanList = new ArrayList<>(); - for (Channel chan : chans) { - chanList.add(chan); - } - createdProperty.setChannels(chanList); - } - return createdProperty; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return propertyService.create(propertyName, property); } @Override public Iterable create(Iterable properties) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_PROPERTY)) { - long start = System.currentTimeMillis(); - propertyManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - - // check if authorized owner - checkPropertiesAuthorization(properties); - - // Validate request parameters - validatePropertyRequest(properties); - - // delete existing property - for (Property property : properties) { - if (propertyRepository.existsById(property.getName())) { - // delete existing property - propertyRepository.deleteById(property.getName()); - } - } - - // create new properties - propertyRepository.indexAll(Lists.newArrayList(properties)); - - // update the listed channels in the properties' payloads with the new - // properties - Map channels = new HashMap<>(); - for (Property property : properties) { - for (Channel ch : property.getChannels()) { - if (channels.containsKey(ch.getName())) { - channels.get(ch.getName()).addProperties(ch.getProperties()); - } else { - channels.put(ch.getName(), ch); - } - } - } - - if (!channels.isEmpty()) { - Iterable chans = channelRepository.saveAll(channels.values()); - } - // TODO should return created props with properly organized saved channels, but it would be - // very complicated... - return properties; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTIES, properties); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return propertyService.create(properties); } @Override public Property addSingle(String propertyName, String channelName, Property property) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_PROPERTY)) { - long start = System.currentTimeMillis(); - propertyManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - // Validate request parameters - validatePropertyRequest(channelName); - if (!propertyName.equals(property.getName()) - || property.getValue().isEmpty() - || property.getValue() == null) { - String message = - MessageFormat.format( - TextUtil.PAYLOAD_PROPERTY_DOES_NOT_MATCH_URI_OR_HAS_BAD_VALUE, property.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); - } - - // check if authorized owner - Optional existingProperty = propertyRepository.findById(propertyName); - boolean present = existingProperty.isPresent(); - if (present) { - checkPropertyAuthorization(existingProperty); - // add property to channel - Channel channel = channelRepository.findById(channelName).get(); - Property prop = existingProperty.get(); - channel.addProperty(new Property(prop.getName(), prop.getOwner(), property.getValue())); - Channel taggedChannel = channelRepository.save(channel); - Property addedProperty = new Property(prop.getName(), prop.getOwner(), property.getValue()); - taggedChannel.setTags(new ArrayList<>()); - taggedChannel.setProperties(new ArrayList<>()); - addedProperty.setChannels(Arrays.asList(taggedChannel)); - return addedProperty; - } else { - String message = MessageFormat.format(TextUtil.PROPERTY_NAME_DOES_NOT_EXIST, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return propertyService.addSingle(propertyName, channelName, property); } @Override public Property update(String propertyName, Property property) { - // check if authorized role - if (!authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_PROPERTY)) { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - - long start = System.currentTimeMillis(); - propertyManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - // Validate request parameters - validatePropertyRequest(property); - - // check if authorized owner - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), property)) { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, property.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - - List chans = new ArrayList<>(); - Optional existingProperty = propertyRepository.findById(propertyName, true); - Property newProperty; - if (existingProperty.isPresent()) { - checkPropertyAuthorization(existingProperty); - chans = existingProperty.get().getChannels(); - newProperty = existingProperty.get(); - newProperty.setOwner(property.getOwner()); - // Is an existing channel being renamed - if (!property.getName().equalsIgnoreCase(existingProperty.get().getName())) { - // Since this is a rename operation we will need to remove the old channel. - propertyRepository.deleteById(existingProperty.get().getName()); - newProperty.setName(property.getName()); - } - } else { - newProperty = property; - } - - // update property - Property updatedProperty = propertyRepository.save(newProperty); - - // update channels of existing property - if (!chans.isEmpty()) { - List chanList = new ArrayList<>(); - for (Channel chan : chans) { - boolean alreadyUpdated = - updatedProperty.getChannels().stream() - .anyMatch(c -> c.getName().equals(chan.getName())); - - if (!alreadyUpdated) { - Optional value = - chan.getProperties().stream() - .filter(p -> p.getName().equals(propertyName)) - .findFirst() - .map(Property::getValue); - if (value.isPresent()) { - Property prop = new Property(property.getName(), property.getOwner(), value.get()); - chan.setProperties(List.of(prop)); - chanList.add(chan); - } - } - } - if (!chanList.isEmpty()) channelRepository.saveAll(chanList); - } - - if (!property.getChannels().isEmpty()) { - // update the listed channels in the property's payloads with the new property - Iterable channels = channelRepository.saveAll(property.getChannels()); - List chanList = new ArrayList<>(); - Property p = null; - for (Channel chan : channels) { - chan.setTags(new ArrayList<>()); - for (Property prop : chan.getProperties()) { - if (prop.getName().equals(propertyName)) p = prop; - } - chan.setProperties(Collections.singletonList(p)); - chanList.add(chan); - } - if (!chanList.isEmpty()) updatedProperty.setChannels(chanList); - } - - return updatedProperty; + return propertyService.update(propertyName, property); } @Override public Iterable update(Iterable properties) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_PROPERTY)) { - long start = System.currentTimeMillis(); - propertyManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - - // check if authorized owner - checkPropertiesAuthorization(properties); - - // Validate request parameters - validatePropertyRequest(properties); - - // prepare the list of channels which need to be updated with the new properties - Map channels = new HashMap<>(); - - // import the old properties - for (Property property : properties) { - if (propertyRepository.existsById(property.getName())) { - for (Channel ch : - propertyRepository.findById(property.getName(), true).get().getChannels()) { - if (channels.containsKey(ch.getName())) { - channels.get(ch.getName()).addProperties(ch.getProperties()); - } else { - channels.put(ch.getName(), ch); - } - } - } - } - // set the new properties - for (Property property : properties) { - for (Channel ch : property.getChannels()) { - if (channels.containsKey(ch.getName())) { - channels.get(ch.getName()).addProperties(ch.getProperties()); - } else { - channels.put(ch.getName(), ch); - } - } - } - - // update properties - Iterable updatedProperties = propertyRepository.saveAll(properties); - - // update channels - if (!channels.isEmpty()) { - channelRepository.saveAll(channels.values()); - } - // TODO should return updated props with properly organized saved channels, but it would be - // very complicated... - return properties; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTIES, properties); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - - private void checkPropertiesAuthorization(Iterable properties) { - for (Property property : properties) { - Optional existingProperty = propertyRepository.findById(property.getName()); - boolean present = existingProperty.isPresent(); - if (present) { - checkPropertyAuthorization(existingProperty); - property.setOwner(existingProperty.get().getOwner()); - property - .getChannels() - .forEach( - chan -> chan.getProperties().get(0).setOwner(existingProperty.get().getOwner())); - } else { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), property)) { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, property.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - } - } - - private void checkPropertyAuthorization(Optional existingProperty) { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingProperty.get())) { - String message = - MessageFormat.format( - TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, existingProperty.get().toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return propertyService.update(properties); } @Override public void remove(String propertyName) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_PROPERTY)) { - Optional existingProperty = propertyRepository.findById(propertyName); - if (existingProperty.isPresent()) { - // check if authorized owner - if (authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingProperty.get())) { - // delete property - propertyRepository.deleteById(propertyName); - } else { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } else { - String message = MessageFormat.format(TextUtil.PROPERTY_NAME_DOES_NOT_EXIST, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - - @Override - public void removeSingle(final String propertyName, String channelName) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_PROPERTY)) { - Optional existingProperty = propertyRepository.findById(propertyName); - if (existingProperty.isPresent()) { - // check if authorized owner - if (authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingProperty.get())) { - Optional ch = channelRepository.findById(channelName); - if (ch.isPresent()) { - // remove property from channel - Channel channel = ch.get(); - channel.removeProperty(new Property(propertyName, "")); - channelRepository.index(channel); - } else { - String message = - MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } else { - String message = MessageFormat.format(TextUtil.PROPERTY_NAME_DOES_NOT_EXIST, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, propertyName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - - /** - * Checks if 1. the property name is not null and matches the name in the body 2. the property - * owner is not null or empty 3. all the listed channels exist and have the property with a non - * null and non empty value - * - * @param property validate property - */ - @Override - public void validatePropertyRequest(Property property) { - // 1 - if (property.getName() == null || property.getName().isEmpty()) { - String message = - MessageFormat.format(TextUtil.PROPERTY_NAME_CANNOT_BE_NULL_OR_EMPTY, property.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message, null); - } - // 2 - if (property.getOwner() == null || property.getOwner().isEmpty()) { - String message = - MessageFormat.format(TextUtil.PROPERTY_OWNER_CANNOT_BE_NULL_OR_EMPTY, property.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message, null); - } - // 3 - property.getChannels().stream() - .forEach( - (channel) -> { - // Check if all the channels exists - if (!channelRepository.existsById(channel.getName())) { - String message = - MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channel.getName()); - logger.log( - Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); - } - // Check if the channel data has the requested property attached with a non null - non - // empty value - if (!channel.getProperties().stream() - .anyMatch( - p -> - p.getName().equals(property.getName()) - && p.getValue() != null - && !p.getValue().isEmpty())) { - String message = - MessageFormat.format( - TextUtil.CHANNEL_NAME_NO_VALID_INSTANCE_PROPERTY, - channel.getName(), - property.toLog()); - logger.log( - Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); - } - }); - } - - /** - * Checks if 1. the property name is not null and matches the name in the body 2. the property - * owner is not null or empty 3. the property value is not null or empty 4. all the listed - * channels exist - * - * @param properties properties to be validated - */ - @Override - public void validatePropertyRequest(Iterable properties) { - for (Property property : properties) { - validatePropertyRequest(property); - } + propertyService.remove(propertyName); } - /** - * Checks if the channel exists - * - * @param channelName check channel exists - */ @Override - public void validatePropertyRequest(String channelName) { - if (!channelRepository.existsById(channelName)) { - String message = MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } + public void removeSingle(String propertyName, String channelName) { + propertyService.removeSingle(propertyName, channelName); } } diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/TagController.java b/src/main/java/org/phoebus/channelfinder/rest/controller/TagController.java index 7bc542e8..5bbb6ad3 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/TagController.java +++ b/src/main/java/org/phoebus/channelfinder/rest/controller/TagController.java @@ -1,534 +1,67 @@ package org.phoebus.channelfinder.rest.controller; -import com.google.common.collect.Lists; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; -import org.phoebus.channelfinder.common.TextUtil; -import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.repository.ChannelRepository; -import org.phoebus.channelfinder.repository.TagRepository; import org.phoebus.channelfinder.rest.api.ITag; -import org.phoebus.channelfinder.service.AuthorizationService; -import org.phoebus.channelfinder.service.AuthorizationService.ROLES; -import org.springframework.beans.factory.annotation.Autowired; +import org.phoebus.channelfinder.service.TagService; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; @CrossOrigin @RestController @EnableAutoConfiguration +@RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/tags") public class TagController implements ITag { - private static final Logger tagManagerAudit = - Logger.getLogger(TagController.class.getName() + ".audit"); - private static final Logger logger = Logger.getLogger(TagController.class.getName()); + private final TagService tagService; - @Autowired TagRepository tagRepository; - - @Autowired ChannelRepository channelRepository; - - @Autowired AuthorizationService authorizationService; + public TagController(TagService tagService) { + this.tagService = tagService; + } @Override public Iterable list() { - return tagRepository.findAll(); + return tagService.list(); } @Override public Tag read(String tagName, boolean withChannels) { - tagManagerAudit.log(Level.INFO, () -> MessageFormat.format(TextUtil.FIND_TAG, tagName)); - - if (withChannels) { - Optional foundTag = tagRepository.findById(tagName, true); - if (foundTag.isPresent()) { - return foundTag.get(); - } else { - String message = MessageFormat.format(TextUtil.TAG_NAME_DOES_NOT_EXIST, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - Optional foundTag = tagRepository.findById(tagName); - if (foundTag.isPresent()) { - return foundTag.get(); - } else { - String message = MessageFormat.format(TextUtil.TAG_NAME_DOES_NOT_EXIST, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } + return tagService.read(tagName, withChannels); } @Override public Tag create(String tagName, Tag tag) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_TAG)) { - long start = System.currentTimeMillis(); - tagManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - // Validate request parameters - validateTagRequest(tag); - - // check if authorized owner - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), tag)) { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tag.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - Optional existingTag = tagRepository.findById(tagName); - boolean present = existingTag.isPresent(); - if (present) { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingTag.get())) { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, existingTag.get().toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - // delete existing tag - tagRepository.deleteById(tagName); - } - - // create new tag - Tag createdTag = tagRepository.index(tag); - - if (!tag.getChannels().isEmpty()) { - tag.getChannels().forEach(chan -> chan.addTag(createdTag)); - // update the listed channels in the tag's payloads with the new tag - Iterable chans = channelRepository.saveAll(tag.getChannels()); - List chanList = new ArrayList<>(); - for (Channel chan : chans) { - chanList.add(chan); - } - createdTag.setChannels(chanList); - } - return createdTag; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return tagService.create(tagName, tag); } @Override public Iterable create(Iterable tags) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_TAG)) { - long start = System.currentTimeMillis(); - tagManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - - // check if authorized owner - for (Tag tag : tags) { - Optional existingTag = tagRepository.findById(tag.getName()); - boolean present = existingTag.isPresent(); - if (present) { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingTag.get())) { - String message = - MessageFormat.format( - TextUtil.USER_NOT_AUTHORIZED_ON_TAG, existingTag.get().toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - tag.setOwner(existingTag.get().getOwner()); - } else { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), tag)) { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tag.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - } - - // Validate request parameters - validateTagRequest(tags); - - // delete existing tags - for (Tag tag : tags) { - if (tagRepository.existsById(tag.getName())) { - // delete existing tag - tagRepository.deleteById(tag.getName()); - } - } - - // create new tags - Iterable createdTags = tagRepository.indexAll(Lists.newArrayList(tags)); - - // update the listed channels in the tags' payloads with new tags - Map channels = new HashMap<>(); - for (Tag tag : tags) { - for (Channel channel : tag.getChannels()) { - if (channels.get(channel.getName()) != null) { - channels.get(channel.getName()).addTag(new Tag(tag.getName(), tag.getOwner())); - } else { - channel.addTag(new Tag(tag.getName(), tag.getOwner())); - channels.put(channel.getName(), channel); - } - } - } - - if (!channels.isEmpty()) { - Iterable chans = channelRepository.saveAll(channels.values()); - } - // TODO should return created tags with properly organized saved channels, but it would be - // very complicated... - return tags; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAGS, tags); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return tagService.create(tags); } @Override public Tag addSingle(String tagName, String channelName) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_TAG)) { - long start = System.currentTimeMillis(); - tagManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - // Validate request parameters - validateTagWithChannelRequest(channelName); - - // check if authorized owner - Optional existingTag = tagRepository.findById(tagName); - boolean present = existingTag.isPresent(); - if (present) { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingTag.get())) { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, existingTag.get().toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - // add tag to channel - Channel channel = channelRepository.findById(channelName).get(); - channel.addTag(existingTag.get()); - Channel taggedChannel = channelRepository.save(channel); - Tag addedTag = existingTag.get(); - addedTag.setChannels(Arrays.asList(taggedChannel)); - return addedTag; - } else { - String message = MessageFormat.format(TextUtil.TAG_NAME_DOES_NOT_EXIST, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return tagService.addSingle(tagName, channelName); } @Override public Tag update(String tagName, Tag tag) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_TAG)) { - long start = System.currentTimeMillis(); - tagManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - // Validate request parameters - validateTagRequest(tag); - - // check if authorized owner - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), tag)) { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tag.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - List channels = new ArrayList<>(); - Optional existingTag = tagRepository.findById(tagName, true); - - Tag newTag; - if (existingTag.isPresent()) { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingTag.get())) { - String message = - MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, existingTag.get().toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - channels = existingTag.get().getChannels(); - newTag = existingTag.get(); - newTag.setOwner(tag.getOwner()); - // Is an existing tag being renamed - if (!tag.getName().equalsIgnoreCase(existingTag.get().getName())) { - // Since this is a rename operation we will need to remove the old channel. - tagRepository.deleteById(existingTag.get().getName()); - newTag.setName(tag.getName()); - } - } else { - newTag = tag; - } - - // update tag - Tag updatedTag = tagRepository.save(newTag); - - // update channels of existing tag - if (!channels.isEmpty()) { - channels.forEach(chan -> chan.addTag(updatedTag)); - } - - // update the listed channels in the tag's payload with the updated tag - if (!tag.getChannels().isEmpty()) { - tag.getChannels().forEach(c -> c.addTag(updatedTag)); - // update the listed channels in the tag's payloads with the new tag - channels.addAll(tag.getChannels()); - } - - if (!channels.isEmpty()) { - Iterable updatedChannels = channelRepository.saveAll(channels); - updatedTag.setChannels( - StreamSupport.stream(updatedChannels.spliterator(), false) - .collect(Collectors.toList())); - } - - return updatedTag; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return tagService.update(tagName, tag); } @Override public Iterable update(Iterable tags) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_TAG)) { - long start = System.currentTimeMillis(); - tagManagerAudit.log( - Level.INFO, - () -> - MessageFormat.format( - TextUtil.CLIENT_INITIALIZATION, (System.currentTimeMillis() - start))); - - // check if authorized owner - for (Tag tag : tags) { - Optional existingTag = tagRepository.findById(tag.getName()); - boolean present = existingTag.isPresent(); - if (present) { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingTag.get())) { - String message = - MessageFormat.format( - TextUtil.USER_NOT_AUTHORIZED_ON_TAG, existingTag.get().toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - tag.setOwner(existingTag.get().getOwner()); - } else { - if (!authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), tag)) { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tag.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - } - - // Validate request parameters - validateTagRequest(tags); - - // update the listed channels in the tags' payloads with new tags - Map channels = new HashMap<>(); - for (Tag tag : tags) { - for (Channel channel : tag.getChannels()) { - if (channels.get(channel.getName()) != null) { - channels.get(channel.getName()).addTag(new Tag(tag.getName(), tag.getOwner())); - } else { - channel.addTag(new Tag(tag.getName(), tag.getOwner())); - channels.put(channel.getName(), channel); - } - } - } - - // update tags - Iterable updatedTags = tagRepository.saveAll(tags); - - // update channels - if (!channels.isEmpty()) { - channelRepository.saveAll(channels.values()); - } - // TODO should return updated tags with properly organized saved channels, but it would be - // very complicated... - return tags; - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAGS, tags); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } + return tagService.update(tags); } @Override public void remove(String tagName) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_TAG)) { - Optional existingTag = tagRepository.findById(tagName); - if (existingTag.isPresent()) { - // check if authorized owner - if (authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingTag.get())) { - // delete tag - tagRepository.deleteById(tagName); - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } else { - String message = MessageFormat.format(TextUtil.TAG_NAME_DOES_NOT_EXIST, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - - @Override - public void removeSingle(final String tagName, String channelName) { - // check if authorized role - if (authorizationService.isAuthorizedRole( - SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_TAG)) { - Optional existingTag = tagRepository.findById(tagName); - if (existingTag.isPresent()) { - // check if authorized owner - if (authorizationService.isAuthorizedOwner( - SecurityContextHolder.getContext().getAuthentication(), existingTag.get())) { - Optional ch = channelRepository.findById(channelName); - if (ch.isPresent()) { - // remove tag from channel - Channel channel = ch.get(); - channel.removeTag(new Tag(tagName, "")); - channelRepository.index(channel); - } else { - String message = - MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } else { - String message = MessageFormat.format(TextUtil.TAG_NAME_DOES_NOT_EXIST, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } else { - String message = MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tagName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.UNAUTHORIZED)); - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message, null); - } - } - - /** - * Checks if all the tags included satisfy the following conditions - * - *

    - *
  1. the tag names are not null or empty and matches the names in the bodies - *
  2. the tag owners are not null or empty - *
  3. all the channels exist - *
- * - * @param tags the list of tags to be validated - */ - @Override - public void validateTagRequest(Iterable tags) { - for (Tag tag : tags) { - validateTagRequest(tag); - } - } - - /** - * Checks if tag satisfies the following conditions - * - *
    - *
  1. the tag name is not null or empty and matches the name in the body - *
  2. the tag owner is not null or empty - *
  3. all the listed channels exist - *
- * - * @param tag the tag to be validates - */ - @Override - public void validateTagRequest(Tag tag) { - // 1 - if (tag.getName() == null || tag.getName().isEmpty()) { - String message = MessageFormat.format(TextUtil.TAG_NAME_CANNOT_BE_NULL_OR_EMPTY, tag.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message, null); - } - // 2 - if (tag.getOwner() == null || tag.getOwner().isEmpty()) { - String message = - MessageFormat.format(TextUtil.TAG_OWNER_CANNOT_BE_NULL_OR_EMPTY, tag.toLog()); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.BAD_REQUEST)); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message, null); - } - // 3 - List channelNames = - tag.getChannels().stream().map(Channel::getName).collect(Collectors.toList()); - for (String channelName : channelNames) { - if (!channelRepository.existsById(channelName)) { - String message = MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } - } + tagService.remove(tagName); } - /** - * Checks if channel with name "channelName" exists - * - * @param channelName check channel exists - */ @Override - public void validateTagWithChannelRequest(String channelName) { - if (!channelRepository.existsById(channelName)) { - String message = MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channelName); - logger.log(Level.SEVERE, message, new ResponseStatusException(HttpStatus.NOT_FOUND)); - throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); - } + public void removeSingle(String tagName, String channelName) { + tagService.removeSingle(tagName, channelName); } } diff --git a/src/main/java/org/phoebus/channelfinder/service/ChannelProcessorService.java b/src/main/java/org/phoebus/channelfinder/service/ChannelProcessorService.java index d06c7805..846b9684 100644 --- a/src/main/java/org/phoebus/channelfinder/service/ChannelProcessorService.java +++ b/src/main/java/org/phoebus/channelfinder/service/ChannelProcessorService.java @@ -10,12 +10,18 @@ import java.util.stream.Collectors; import org.phoebus.channelfinder.configuration.ChannelProcessor; import org.phoebus.channelfinder.entity.Channel; +import org.phoebus.channelfinder.entity.Scroll; +import org.phoebus.channelfinder.exceptions.UnauthorizedException; +import org.phoebus.channelfinder.service.AuthorizationService.ROLES; import org.phoebus.channelfinder.service.model.archiver.ChannelProcessorInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.task.TaskExecutor; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; @Service public class ChannelProcessorService { @@ -23,18 +29,50 @@ public class ChannelProcessorService { private static final Logger logger = Logger.getLogger(ChannelProcessorService.class.getName()); private final List channelProcessors; - private final TaskExecutor channelFinderTaskExecutor; - + private final AuthorizationService authorizationService; + private final ChannelScrollService channelScrollService; private final int chunkSize; + private final int defaultMaxSize; public ChannelProcessorService( @Autowired List channelProcessors, @Autowired @Qualifier("channelFinderTaskExecutor") TaskExecutor channelFinderTaskExecutor, - @Value("${processors.chunking.size:10000}") int chunkSize) { + @Autowired AuthorizationService authorizationService, + @Autowired ChannelScrollService channelScrollService, + @Value("${processors.chunking.size:10000}") int chunkSize, + @Value("${elasticsearch.query.size:10000}") int defaultMaxSize) { this.channelProcessors = channelProcessors; this.channelFinderTaskExecutor = channelFinderTaskExecutor; + this.authorizationService = authorizationService; + this.channelScrollService = channelScrollService; this.chunkSize = chunkSize; + this.defaultMaxSize = defaultMaxSize; + } + + public long processAllChannels() { + if (!authorizationService.isAuthorizedRole( + SecurityContextHolder.getContext().getAuthentication(), ROLES.CF_ADMIN)) { + throw new UnauthorizedException( + "User does not have the proper authorization to perform this operation: /process/all"); + } + logger.log(Level.INFO, "Calling processor on ALL channels in ChannelFinder"); + MultiValueMap searchParameters = new LinkedMultiValueMap<>(); + searchParameters.add("~name", "*"); + return processChannelsByQuery(searchParameters); + } + + public long processChannelsByQuery(MultiValueMap allRequestParams) { + long channelCount = 0; + Scroll scrollResult = channelScrollService.search(null, allRequestParams); + channelCount += scrollResult.getChannels().size(); + sendToProcessors(scrollResult.getChannels()); + while (scrollResult.getChannels().size() == defaultMaxSize) { + scrollResult = channelScrollService.search(scrollResult.getId(), allRequestParams); + channelCount += scrollResult.getChannels().size(); + sendToProcessors(scrollResult.getChannels()); + } + return channelCount; } public long getProcessorCount() { diff --git a/src/main/java/org/phoebus/channelfinder/service/ChannelScrollService.java b/src/main/java/org/phoebus/channelfinder/service/ChannelScrollService.java new file mode 100644 index 00000000..68369ace --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/service/ChannelScrollService.java @@ -0,0 +1,20 @@ +package org.phoebus.channelfinder.service; + +import org.phoebus.channelfinder.entity.Scroll; +import org.phoebus.channelfinder.repository.ChannelRepository; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; + +@Service +public class ChannelScrollService { + + private final ChannelRepository channelRepository; + + public ChannelScrollService(ChannelRepository channelRepository) { + this.channelRepository = channelRepository; + } + + public Scroll search(String scrollId, MultiValueMap searchParameters) { + return channelRepository.scroll(scrollId, searchParameters); + } +} diff --git a/src/main/java/org/phoebus/channelfinder/service/ChannelService.java b/src/main/java/org/phoebus/channelfinder/service/ChannelService.java new file mode 100644 index 00000000..039cca32 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/service/ChannelService.java @@ -0,0 +1,290 @@ +package org.phoebus.channelfinder.service; + +import com.google.common.collect.Lists; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.phoebus.channelfinder.common.TextUtil; +import org.phoebus.channelfinder.entity.Channel; +import org.phoebus.channelfinder.entity.Property; +import org.phoebus.channelfinder.entity.SearchResult; +import org.phoebus.channelfinder.entity.Tag; +import org.phoebus.channelfinder.exceptions.ChannelNotFoundException; +import org.phoebus.channelfinder.exceptions.ChannelValidationException; +import org.phoebus.channelfinder.exceptions.PropertyNotFoundException; +import org.phoebus.channelfinder.exceptions.TagNotFoundException; +import org.phoebus.channelfinder.exceptions.UnauthorizedException; +import org.phoebus.channelfinder.repository.ChannelRepository; +import org.phoebus.channelfinder.repository.PropertyRepository; +import org.phoebus.channelfinder.repository.TagRepository; +import org.phoebus.channelfinder.service.AuthorizationService.ROLES; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; + +@Service +public class ChannelService { + + private static final Logger audit = Logger.getLogger(ChannelService.class.getName() + ".audit"); + private static final Logger logger = Logger.getLogger(ChannelService.class.getName()); + + private final ChannelRepository channelRepository; + private final TagRepository tagRepository; + private final PropertyRepository propertyRepository; + private final AuthorizationService authorizationService; + private final ChannelProcessorService channelProcessorService; + + public ChannelService( + ChannelRepository channelRepository, + TagRepository tagRepository, + PropertyRepository propertyRepository, + AuthorizationService authorizationService, + ChannelProcessorService channelProcessorService) { + this.channelRepository = channelRepository; + this.tagRepository = tagRepository; + this.propertyRepository = propertyRepository; + this.authorizationService = authorizationService; + this.channelProcessorService = channelProcessorService; + } + + public List query(MultiValueMap allRequestParams) { + return channelRepository.search(allRequestParams).channels(); + } + + public SearchResult combinedQuery(MultiValueMap allRequestParams) { + return channelRepository.search(allRequestParams); + } + + public long queryCount(MultiValueMap allRequestParams) { + return channelRepository.count(allRequestParams); + } + + public Channel read(String channelName) { + audit.log(Level.INFO, () -> MessageFormat.format(TextUtil.FIND_CHANNEL, channelName)); + return channelRepository + .findById(channelName) + .orElseThrow(() -> new ChannelNotFoundException(channelName)); + } + + public Channel create(String channelName, Channel channel) { + requireRole(ROLES.CF_CHANNEL, channelName); + audit.log(Level.INFO, () -> MessageFormat.format(TextUtil.CREATE_CHANNEL, channel.toLog())); + + validateChannel(channel); + requireOwner(channel); + + Optional existingChannel = channelRepository.findById(channelName); + if (existingChannel.isPresent()) { + requireOwner(existingChannel.get()); + channelRepository.deleteById(channelName); + } + + resetOwnersToExisting(List.of(channel)); + + Channel created = channelRepository.index(channel); + channelProcessorService.sendToProcessors(List.of(created)); + return created; + } + + public Iterable create(Iterable channels) { + requireRole(ROLES.CF_CHANNEL, "channels batch"); + + Map existing = + channelRepository + .findAllById( + StreamSupport.stream(channels.spliterator(), true).map(Channel::getName).toList()) + .stream() + .collect(Collectors.toMap(Channel::getName, c -> c)); + + for (Channel channel : channels) { + if (existing.containsKey(channel.getName())) { + requireOwner(existing.get(channel.getName())); + channel.setOwner(existing.get(channel.getName()).getOwner()); + } else { + requireOwner(channel); + } + } + + validateChannels(channels); + channelRepository.deleteAll(channels); + resetOwnersToExisting(channels); + + List created = channelRepository.indexAll(Lists.newArrayList(channels)); + channelProcessorService.sendToProcessors(created); + return created; + } + + public Channel update(String channelName, Channel channel) { + requireRole(ROLES.CF_CHANNEL, channelName); + validateChannel(channel); + requireOwner(channel); + + Optional existingChannel = channelRepository.findById(channelName); + + Channel newChannel; + if (existingChannel.isPresent()) { + requireOwner(existingChannel.get()); + newChannel = existingChannel.get(); + newChannel.setOwner(channel.getOwner()); + newChannel.addProperties(channel.getProperties()); + newChannel.addTags(channel.getTags()); + if (!channel.getName().equalsIgnoreCase(existingChannel.get().getName())) { + channelRepository.deleteById(existingChannel.get().getName()); + newChannel.setName(channel.getName()); + } + } else { + newChannel = channel; + } + + resetOwnersToExisting(List.of(channel)); + + Channel updated = channelRepository.save(newChannel); + channelProcessorService.sendToProcessors(List.of(updated)); + return updated; + } + + public Iterable update(Iterable channels) { + requireRole(ROLES.CF_CHANNEL, "channels batch"); + + for (Channel channel : channels) { + Optional existing = channelRepository.findById(channel.getName()); + if (existing.isPresent()) { + requireOwner(existing.get()); + channel.setOwner(existing.get().getOwner()); + } else { + requireOwner(channel); + } + } + + validateChannels(channels); + resetOwnersToExisting(channels); + + List updated = Lists.newArrayList(channelRepository.saveAll(channels)); + channelProcessorService.sendToProcessors(updated); + return updated; + } + + public void remove(String channelName) { + requireRole(ROLES.CF_CHANNEL, channelName); + audit.log(Level.INFO, () -> MessageFormat.format(TextUtil.DELETE_CHANNEL, channelName)); + + Channel existing = + channelRepository + .findById(channelName) + .orElseThrow(() -> new ChannelNotFoundException(channelName)); + requireOwner(existing); + channelRepository.deleteById(channelName); + } + + private void validateChannel(Channel channel) { + if (channel.getName() == null || channel.getName().isEmpty()) { + throw new ChannelValidationException( + MessageFormat.format(TextUtil.CHANNEL_NAME_CANNOT_BE_NULL_OR_EMPTY, channel.toLog())); + } + if (channel.getOwner() == null || channel.getOwner().isEmpty()) { + throw new ChannelValidationException( + MessageFormat.format(TextUtil.CHANNEL_OWNER_CANNOT_BE_NULL_OR_EMPTY, channel.toLog())); + } + for (Tag tag : channel.getTags()) { + if (!tagRepository.existsById(tag.getName())) { + throw new TagNotFoundException(tag.getName()); + } + } + checkPropertyValues(channel); + } + + private void validateChannels(Iterable channels) { + List existingProperties = + StreamSupport.stream(propertyRepository.findAll().spliterator(), true) + .map(Property::getName) + .toList(); + List existingTags = + StreamSupport.stream(tagRepository.findAll().spliterator(), true) + .map(Tag::getName) + .toList(); + for (Channel channel : channels) { + validateChannelAgainst(channel, existingTags, existingProperties); + } + } + + private void validateChannelAgainst( + Channel channel, List existingTags, List existingProperties) { + if (channel.getName() == null || channel.getName().isEmpty()) { + throw new ChannelValidationException( + MessageFormat.format(TextUtil.CHANNEL_NAME_CANNOT_BE_NULL_OR_EMPTY, channel.toLog())); + } + if (channel.getOwner() == null || channel.getOwner().isEmpty()) { + throw new ChannelValidationException( + MessageFormat.format(TextUtil.CHANNEL_OWNER_CANNOT_BE_NULL_OR_EMPTY, channel.toLog())); + } + for (Tag tag : channel.getTags()) { + if (!existingTags.contains(tag.getName())) { + throw new TagNotFoundException(tag.getName()); + } + } + List propNames = channel.getProperties().stream().map(Property::getName).toList(); + List propValues = channel.getProperties().stream().map(Property::getValue).toList(); + for (String propName : propNames) { + if (!existingProperties.contains(propName)) { + throw new PropertyNotFoundException(propName); + } + } + checkValues(propNames, propValues); + } + + private void checkPropertyValues(Channel channel) { + List propNames = channel.getProperties().stream().map(Property::getName).toList(); + List propValues = channel.getProperties().stream().map(Property::getValue).toList(); + for (String propName : propNames) { + if (!propertyRepository.existsById(propName)) { + throw new PropertyNotFoundException(propName); + } + } + checkValues(propNames, propValues); + } + + private void checkValues(List propNames, List propValues) { + for (int i = 0; i < propValues.size(); i++) { + String value = propValues.get(i); + if (value == null || value.isEmpty()) { + throw new ChannelValidationException( + MessageFormat.format(TextUtil.PROPERTY_VALUE_NULL_OR_EMPTY, propNames.get(i), value)); + } + } + } + + private void requireRole(ROLES role, Object subject) { + if (!authorizationService.isAuthorizedRole( + SecurityContextHolder.getContext().getAuthentication(), role)) { + throw new UnauthorizedException( + MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, subject)); + } + } + + private void requireOwner(Channel channel) { + if (!authorizationService.isAuthorizedOwner( + SecurityContextHolder.getContext().getAuthentication(), channel)) { + throw new UnauthorizedException( + MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_CHANNEL, channel.toLog())); + } + } + + private void resetOwnersToExisting(Iterable channels) { + Map propOwners = + StreamSupport.stream(propertyRepository.findAll().spliterator(), true) + .collect(Collectors.toUnmodifiableMap(Property::getName, Property::getOwner)); + Map tagOwners = + StreamSupport.stream(tagRepository.findAll().spliterator(), true) + .collect(Collectors.toUnmodifiableMap(Tag::getName, Tag::getOwner)); + + for (Channel channel : channels) { + channel.getProperties().forEach(prop -> prop.setOwner(propOwners.get(prop.getName()))); + channel.getTags().forEach(tag -> tag.setOwner(tagOwners.get(tag.getName()))); + } + } +} diff --git a/src/main/java/org/phoebus/channelfinder/service/InfoService.java b/src/main/java/org/phoebus/channelfinder/service/InfoService.java new file mode 100644 index 00000000..32095430 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/service/InfoService.java @@ -0,0 +1,58 @@ +package org.phoebus.channelfinder.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.phoebus.channelfinder.configuration.ElasticConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class InfoService { + + private static final Logger logger = Logger.getLogger(InfoService.class.getName()); + + private static final ObjectMapper objectMapper = + new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + + private final ElasticConfig esService; + + @Value("${channelfinder.version:4.7.0}") + private String version; + + public InfoService(ElasticConfig esService) { + this.esService = esService; + } + + public String info() { + Map cfServiceInfo = new LinkedHashMap<>(); + cfServiceInfo.put("name", "ChannelFinder Service"); + cfServiceInfo.put("version", version); + + Map elasticInfo = new LinkedHashMap<>(); + try { + var client = esService.getSearchClient(); + var response = client.info(); + elasticInfo.put("status", "Connected"); + elasticInfo.put("clusterName", response.clusterName()); + elasticInfo.put("clusterUuid", response.clusterUuid()); + elasticInfo.put("version", response.version().number()); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to retrieve Elasticsearch info", e); + elasticInfo.put("status", "Failed to connect to elastic " + e.getLocalizedMessage()); + } + cfServiceInfo.put("elastic", elasticInfo); + + try { + return objectMapper.writeValueAsString(cfServiceInfo); + } catch (JsonProcessingException e) { + logger.log(Level.WARNING, "Failed to serialize ChannelFinder service info", e); + return "Failed to gather ChannelFinder service info"; + } + } +} diff --git a/src/main/java/org/phoebus/channelfinder/service/PropertyService.java b/src/main/java/org/phoebus/channelfinder/service/PropertyService.java new file mode 100644 index 00000000..95dfd95b --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/service/PropertyService.java @@ -0,0 +1,341 @@ +package org.phoebus.channelfinder.service; + +import com.google.common.collect.Lists; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.phoebus.channelfinder.common.TextUtil; +import org.phoebus.channelfinder.entity.Channel; +import org.phoebus.channelfinder.entity.Property; +import org.phoebus.channelfinder.exceptions.ChannelNotFoundException; +import org.phoebus.channelfinder.exceptions.PropertyNotFoundException; +import org.phoebus.channelfinder.exceptions.PropertyValidationException; +import org.phoebus.channelfinder.exceptions.UnauthorizedException; +import org.phoebus.channelfinder.repository.ChannelRepository; +import org.phoebus.channelfinder.repository.PropertyRepository; +import org.phoebus.channelfinder.service.AuthorizationService.ROLES; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class PropertyService { + + private static final Logger audit = Logger.getLogger(PropertyService.class.getName() + ".audit"); + private static final Logger logger = Logger.getLogger(PropertyService.class.getName()); + + private final PropertyRepository propertyRepository; + private final ChannelRepository channelRepository; + private final AuthorizationService authorizationService; + + public PropertyService( + PropertyRepository propertyRepository, + ChannelRepository channelRepository, + AuthorizationService authorizationService) { + this.propertyRepository = propertyRepository; + this.channelRepository = channelRepository; + this.authorizationService = authorizationService; + } + + public Iterable list() { + return propertyRepository.findAll(); + } + + public Property read(String propertyName, boolean withChannels) { + audit.log(Level.INFO, () -> MessageFormat.format(TextUtil.FIND_PROPERTY, propertyName)); + Optional found = + withChannels + ? propertyRepository.findById(propertyName, true) + : propertyRepository.findById(propertyName); + return found.orElseThrow(() -> new PropertyNotFoundException(propertyName)); + } + + public Property create(String propertyName, Property property) { + requireRole(ROLES.CF_PROPERTY, propertyName); + validateProperty(property); + requireOwner(property); + + Optional existing = propertyRepository.findById(propertyName); + if (existing.isPresent()) { + requireOwner(existing.get()); + propertyRepository.deleteById(propertyName); + } + + Property created = propertyRepository.index(property); + + if (!property.getChannels().isEmpty()) { + Iterable chans = channelRepository.saveAll(property.getChannels()); + List chanList = new ArrayList<>(); + for (Channel chan : chans) chanList.add(chan); + created.setChannels(chanList); + } + return created; + } + + public Iterable create(Iterable properties) { + requireRole(ROLES.CF_PROPERTY, "properties batch"); + + checkPropertiesAuthorization(properties); + validateProperties(properties); + + for (Property property : properties) { + if (propertyRepository.existsById(property.getName())) { + propertyRepository.deleteById(property.getName()); + } + } + + propertyRepository.indexAll(Lists.newArrayList(properties)); + + Map channels = new HashMap<>(); + for (Property property : properties) { + mergeChannelsIntoMap(property.getChannels(), channels); + } + + if (!channels.isEmpty()) { + channelRepository.saveAll(channels.values()); + } + return properties; + } + + public Property addSingle(String propertyName, String channelName, Property property) { + requireRole(ROLES.CF_PROPERTY, propertyName); + requireChannelExists(channelName); + + if (!propertyName.equals(property.getName()) + || property.getValue() == null + || property.getValue().isEmpty()) { + throw new PropertyValidationException( + MessageFormat.format( + TextUtil.PAYLOAD_PROPERTY_DOES_NOT_MATCH_URI_OR_HAS_BAD_VALUE, property.toLog())); + } + + Property existing = + propertyRepository + .findById(propertyName) + .orElseThrow(() -> new PropertyNotFoundException(propertyName)); + requireOwner(existing); + + Channel channel = channelRepository.findById(channelName).get(); + channel.addProperty(new Property(existing.getName(), existing.getOwner(), property.getValue())); + Channel saved = channelRepository.save(channel); + Property added = new Property(existing.getName(), existing.getOwner(), property.getValue()); + saved.setTags(new ArrayList<>()); + saved.setProperties(new ArrayList<>()); + added.setChannels(Arrays.asList(saved)); + return added; + } + + public Property update(String propertyName, Property property) { + requireRole(ROLES.CF_PROPERTY, propertyName); + validateProperty(property); + requireOwner(property); + + List chans = new ArrayList<>(); + Optional existingOpt = propertyRepository.findById(propertyName, true); + Property newProperty; + if (existingOpt.isPresent()) { + requireOwner(existingOpt.get()); + chans = existingOpt.get().getChannels(); + newProperty = existingOpt.get(); + newProperty.setOwner(property.getOwner()); + if (!property.getName().equalsIgnoreCase(existingOpt.get().getName())) { + propertyRepository.deleteById(existingOpt.get().getName()); + newProperty.setName(property.getName()); + } + } else { + newProperty = property; + } + + Property updated = propertyRepository.save(newProperty); + + propagateRenameToChannels(propertyName, updated, chans); + + if (!property.getChannels().isEmpty()) { + List chanList = saveAndRetainProperty(property.getChannels(), propertyName); + if (!chanList.isEmpty()) updated.setChannels(chanList); + } + + return updated; + } + + private List saveAndRetainProperty(Iterable channels, String propertyName) { + List result = new ArrayList<>(); + for (Channel chan : channelRepository.saveAll(channels)) { + chan.setTags(new ArrayList<>()); + chan.setProperties( + Collections.singletonList( + chan.getProperties().stream() + .filter(p -> p.getName().equals(propertyName)) + .findFirst() + .orElse(null))); + result.add(chan); + } + return result; + } + + public Iterable update(Iterable properties) { + requireRole(ROLES.CF_PROPERTY, "properties batch"); + + checkPropertiesAuthorization(properties); + validateProperties(properties); + + Map channels = new HashMap<>(); + for (Property property : properties) { + if (propertyRepository.existsById(property.getName())) { + mergeChannelsIntoMap( + propertyRepository.findById(property.getName(), true).get().getChannels(), channels); + } + } + for (Property property : properties) { + mergeChannelsIntoMap(property.getChannels(), channels); + } + + propertyRepository.saveAll(properties); + + if (!channels.isEmpty()) { + channelRepository.saveAll(channels.values()); + } + return properties; + } + + public void remove(String propertyName) { + requireRole(ROLES.CF_PROPERTY, propertyName); + + Property existing = + propertyRepository + .findById(propertyName) + .orElseThrow(() -> new PropertyNotFoundException(propertyName)); + requireOwner(existing); + propertyRepository.deleteById(propertyName); + } + + public void removeSingle(String propertyName, String channelName) { + requireRole(ROLES.CF_PROPERTY, propertyName); + + Property existing = + propertyRepository + .findById(propertyName) + .orElseThrow(() -> new PropertyNotFoundException(propertyName)); + requireOwner(existing); + + Channel channel = + channelRepository + .findById(channelName) + .orElseThrow(() -> new ChannelNotFoundException(channelName)); + channel.removeProperty(new Property(propertyName, "")); + channelRepository.index(channel); + } + + private void propagateRenameToChannels( + String oldPropertyName, Property updated, List existingChannels) { + if (existingChannels.isEmpty()) return; + List toUpdate = new ArrayList<>(); + for (Channel chan : existingChannels) { + boolean alreadyUpdated = + updated.getChannels().stream().anyMatch(c -> c.getName().equals(chan.getName())); + if (!alreadyUpdated) { + chan.getProperties().stream() + .filter(p -> p.getName().equals(oldPropertyName)) + .findFirst() + .map(Property::getValue) + .ifPresent( + val -> { + chan.setProperties( + List.of(new Property(updated.getName(), updated.getOwner(), val))); + toUpdate.add(chan); + }); + } + } + if (!toUpdate.isEmpty()) channelRepository.saveAll(toUpdate); + } + + private void mergeChannelsIntoMap(Iterable channels, Map target) { + for (Channel ch : channels) { + if (target.containsKey(ch.getName())) { + target.get(ch.getName()).addProperties(ch.getProperties()); + } else { + target.put(ch.getName(), ch); + } + } + } + + private void validateProperty(Property property) { + if (property.getName() == null || property.getName().isEmpty()) { + throw new PropertyValidationException( + MessageFormat.format(TextUtil.PROPERTY_NAME_CANNOT_BE_NULL_OR_EMPTY, property.toLog())); + } + if (property.getOwner() == null || property.getOwner().isEmpty()) { + throw new PropertyValidationException( + MessageFormat.format(TextUtil.PROPERTY_OWNER_CANNOT_BE_NULL_OR_EMPTY, property.toLog())); + } + for (Channel channel : property.getChannels()) { + if (!channelRepository.existsById(channel.getName())) { + throw new PropertyValidationException( + MessageFormat.format(TextUtil.CHANNEL_NAME_DOES_NOT_EXIST, channel.getName())); + } + boolean hasValidValue = + channel.getProperties().stream() + .anyMatch( + p -> + p.getName().equals(property.getName()) + && p.getValue() != null + && !p.getValue().isEmpty()); + if (!hasValidValue) { + throw new PropertyValidationException( + MessageFormat.format( + TextUtil.CHANNEL_NAME_NO_VALID_INSTANCE_PROPERTY, + channel.getName(), + property.getName())); + } + } + } + + private void validateProperties(Iterable properties) { + for (Property property : properties) { + validateProperty(property); + } + } + + private void requireChannelExists(String channelName) { + if (!channelRepository.existsById(channelName)) { + throw new ChannelNotFoundException(channelName); + } + } + + private void requireRole(ROLES role, Object subject) { + if (!authorizationService.isAuthorizedRole( + SecurityContextHolder.getContext().getAuthentication(), role)) { + throw new UnauthorizedException( + MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, subject)); + } + } + + private void requireOwner(Property property) { + if (!authorizationService.isAuthorizedOwner( + SecurityContextHolder.getContext().getAuthentication(), property)) { + throw new UnauthorizedException( + MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_PROPERTY, property.toLog())); + } + } + + private void checkPropertiesAuthorization(Iterable properties) { + for (Property property : properties) { + Optional existing = propertyRepository.findById(property.getName()); + if (existing.isPresent()) { + requireOwner(existing.get()); + property.setOwner(existing.get().getOwner()); + property + .getChannels() + .forEach(chan -> chan.getProperties().get(0).setOwner(existing.get().getOwner())); + } else { + requireOwner(property); + } + } + } +} diff --git a/src/main/java/org/phoebus/channelfinder/service/TagService.java b/src/main/java/org/phoebus/channelfinder/service/TagService.java new file mode 100644 index 00000000..abc87218 --- /dev/null +++ b/src/main/java/org/phoebus/channelfinder/service/TagService.java @@ -0,0 +1,275 @@ +package org.phoebus.channelfinder.service; + +import com.google.common.collect.Lists; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.StreamSupport; +import org.phoebus.channelfinder.common.TextUtil; +import org.phoebus.channelfinder.entity.Channel; +import org.phoebus.channelfinder.entity.Tag; +import org.phoebus.channelfinder.exceptions.ChannelNotFoundException; +import org.phoebus.channelfinder.exceptions.TagNotFoundException; +import org.phoebus.channelfinder.exceptions.TagValidationException; +import org.phoebus.channelfinder.exceptions.UnauthorizedException; +import org.phoebus.channelfinder.repository.ChannelRepository; +import org.phoebus.channelfinder.repository.TagRepository; +import org.phoebus.channelfinder.service.AuthorizationService.ROLES; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class TagService { + + private static final Logger audit = Logger.getLogger(TagService.class.getName() + ".audit"); + private static final Logger logger = Logger.getLogger(TagService.class.getName()); + + private final TagRepository tagRepository; + private final ChannelRepository channelRepository; + private final AuthorizationService authorizationService; + + public TagService( + TagRepository tagRepository, + ChannelRepository channelRepository, + AuthorizationService authorizationService) { + this.tagRepository = tagRepository; + this.channelRepository = channelRepository; + this.authorizationService = authorizationService; + } + + public Iterable list() { + return tagRepository.findAll(); + } + + public Tag read(String tagName, boolean withChannels) { + audit.log(Level.INFO, () -> MessageFormat.format(TextUtil.FIND_TAG, tagName)); + Optional found = + withChannels ? tagRepository.findById(tagName, true) : tagRepository.findById(tagName); + return found.orElseThrow(() -> new TagNotFoundException(tagName)); + } + + public Tag create(String tagName, Tag tag) { + requireRole(ROLES.CF_TAG, tagName); + validateTag(tag); + requireOwner(tag); + + Optional existingTag = tagRepository.findById(tagName); + if (existingTag.isPresent()) { + requireOwner(existingTag.get()); + tagRepository.deleteById(tagName); + } + + Tag created = tagRepository.index(tag); + + if (!tag.getChannels().isEmpty()) { + tag.getChannels().forEach(chan -> chan.addTag(created)); + Iterable chans = channelRepository.saveAll(tag.getChannels()); + List chanList = new ArrayList<>(); + for (Channel chan : chans) chanList.add(chan); + created.setChannels(chanList); + } + return created; + } + + public Iterable create(Iterable tags) { + requireRole(ROLES.CF_TAG, "tags batch"); + + for (Tag tag : tags) { + Optional existing = tagRepository.findById(tag.getName()); + if (existing.isPresent()) { + requireOwner(existing.get()); + tag.setOwner(existing.get().getOwner()); + } else { + requireOwner(tag); + } + } + + validateTags(tags); + + for (Tag tag : tags) { + if (tagRepository.existsById(tag.getName())) { + tagRepository.deleteById(tag.getName()); + } + } + + tagRepository.indexAll(Lists.newArrayList(tags)); + + Map channels = new HashMap<>(); + for (Tag tag : tags) { + for (Channel channel : tag.getChannels()) { + if (channels.containsKey(channel.getName())) { + channels.get(channel.getName()).addTag(new Tag(tag.getName(), tag.getOwner())); + } else { + channel.addTag(new Tag(tag.getName(), tag.getOwner())); + channels.put(channel.getName(), channel); + } + } + } + + if (!channels.isEmpty()) { + channelRepository.saveAll(channels.values()); + } + return tags; + } + + public Tag addSingle(String tagName, String channelName) { + requireRole(ROLES.CF_TAG, tagName); + requireChannelExists(channelName); + + Tag existing = + tagRepository.findById(tagName).orElseThrow(() -> new TagNotFoundException(tagName)); + requireOwner(existing); + + Channel channel = channelRepository.findById(channelName).get(); + channel.addTag(existing); + Channel saved = channelRepository.save(channel); + existing.setChannels(Arrays.asList(saved)); + return existing; + } + + public Tag update(String tagName, Tag tag) { + requireRole(ROLES.CF_TAG, tagName); + validateTag(tag); + requireOwner(tag); + + List channels = new ArrayList<>(); + Optional existingTag = tagRepository.findById(tagName, true); + + Tag newTag; + if (existingTag.isPresent()) { + requireOwner(existingTag.get()); + channels = existingTag.get().getChannels(); + newTag = existingTag.get(); + newTag.setOwner(tag.getOwner()); + if (!tag.getName().equalsIgnoreCase(existingTag.get().getName())) { + tagRepository.deleteById(existingTag.get().getName()); + newTag.setName(tag.getName()); + } + } else { + newTag = tag; + } + + Tag updated = tagRepository.save(newTag); + + if (!channels.isEmpty()) { + channels.forEach(chan -> chan.addTag(updated)); + } + if (!tag.getChannels().isEmpty()) { + tag.getChannels().forEach(c -> c.addTag(updated)); + channels.addAll(tag.getChannels()); + } + if (!channels.isEmpty()) { + Iterable updatedChannels = channelRepository.saveAll(channels); + updated.setChannels(StreamSupport.stream(updatedChannels.spliterator(), false).toList()); + } + + return updated; + } + + public Iterable update(Iterable tags) { + requireRole(ROLES.CF_TAG, "tags batch"); + + for (Tag tag : tags) { + Optional existing = tagRepository.findById(tag.getName()); + if (existing.isPresent()) { + requireOwner(existing.get()); + tag.setOwner(existing.get().getOwner()); + } else { + requireOwner(tag); + } + } + + validateTags(tags); + + Map channels = new HashMap<>(); + for (Tag tag : tags) { + for (Channel channel : tag.getChannels()) { + if (channels.containsKey(channel.getName())) { + channels.get(channel.getName()).addTag(new Tag(tag.getName(), tag.getOwner())); + } else { + channel.addTag(new Tag(tag.getName(), tag.getOwner())); + channels.put(channel.getName(), channel); + } + } + } + + tagRepository.saveAll(tags); + + if (!channels.isEmpty()) { + channelRepository.saveAll(channels.values()); + } + return tags; + } + + public void remove(String tagName) { + requireRole(ROLES.CF_TAG, tagName); + + Tag existing = + tagRepository.findById(tagName).orElseThrow(() -> new TagNotFoundException(tagName)); + requireOwner(existing); + tagRepository.deleteById(tagName); + } + + public void removeSingle(String tagName, String channelName) { + requireRole(ROLES.CF_TAG, tagName); + + Tag existingTag = + tagRepository.findById(tagName).orElseThrow(() -> new TagNotFoundException(tagName)); + requireOwner(existingTag); + + Channel channel = + channelRepository + .findById(channelName) + .orElseThrow(() -> new ChannelNotFoundException(channelName)); + channel.removeTag(new Tag(tagName, "")); + channelRepository.index(channel); + } + + private void validateTag(Tag tag) { + if (tag.getName() == null || tag.getName().isEmpty()) { + throw new TagValidationException( + MessageFormat.format(TextUtil.TAG_NAME_CANNOT_BE_NULL_OR_EMPTY, tag.toLog())); + } + if (tag.getOwner() == null || tag.getOwner().isEmpty()) { + throw new TagValidationException( + MessageFormat.format(TextUtil.TAG_OWNER_CANNOT_BE_NULL_OR_EMPTY, tag.toLog())); + } + for (Channel channel : tag.getChannels()) { + requireChannelExists(channel.getName()); + } + } + + private void validateTags(Iterable tags) { + for (Tag tag : tags) { + validateTag(tag); + } + } + + private void requireChannelExists(String channelName) { + if (!channelRepository.existsById(channelName)) { + throw new ChannelNotFoundException(channelName); + } + } + + private void requireRole(ROLES role, Object subject) { + if (!authorizationService.isAuthorizedRole( + SecurityContextHolder.getContext().getAuthentication(), role)) { + throw new UnauthorizedException( + MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, subject)); + } + } + + private void requireOwner(Tag tag) { + if (!authorizationService.isAuthorizedOwner( + SecurityContextHolder.getContext().getAuthentication(), tag)) { + throw new UnauthorizedException( + MessageFormat.format(TextUtil.USER_NOT_AUTHORIZED_ON_TAG, tag.toLog())); + } + } +} diff --git a/src/main/java/org/phoebus/channelfinder/service/external/ArchiverService.java b/src/main/java/org/phoebus/channelfinder/service/external/ArchiverService.java index c2ce34a7..e458a1fc 100644 --- a/src/main/java/org/phoebus/channelfinder/service/external/ArchiverService.java +++ b/src/main/java/org/phoebus/channelfinder/service/external/ArchiverService.java @@ -61,11 +61,10 @@ String key() { private List postSupportArchivers; @Autowired - public ArchiverService( - RestClient.Builder builder, @Value("${aa.timeout_seconds:15}") int timeoutSeconds) { + public ArchiverService(@Value("${aa.timeout_seconds:15}") int timeoutSeconds) { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setReadTimeout(timeoutSeconds * 1000); - this.client = builder.requestFactory(factory).build(); + this.client = RestClient.builder().requestFactory(factory).build(); } ArchiverService(RestClient.Builder builder) { diff --git a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java index dafbbb08..ea45f02d 100644 --- a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java +++ b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java @@ -78,10 +78,10 @@ void searchNameTest() throws InterruptedException { MultiValueMap searchParameters = new LinkedMultiValueMap(); // Search for a single unique channel searchParameters.add("~name", channelNames.get(0)); - Scroll scrollResult = channelScroll.search(null, searchParameters); + Scroll scrollResult = channelScroll.query(searchParameters); List result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertTrue( @@ -90,20 +90,20 @@ void searchNameTest() throws InterruptedException { // Search for all channels via wildcards searchParameters.clear(); searchParameters.add("~name", "BR:C001-BI:2{BLA}Pos:?-RB"); - scrollResult = channelScroll.search(null, searchParameters); + scrollResult = channelScroll.query(searchParameters); result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertSame(2, result.size(), "Expected 2 but got " + result.size()); searchParameters.clear(); searchParameters.add("~name", "BR:C001-BI:?{BLA}Pos:*"); - scrollResult = channelScroll.search(null, searchParameters); + scrollResult = channelScroll.query(searchParameters); result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertSame(4, result.size(), "Expected 4 but got " + result.size()); @@ -111,10 +111,10 @@ void searchNameTest() throws InterruptedException { // Search for all 1000 channels searchParameters.clear(); searchParameters.add("~name", "SR*"); - scrollResult = channelScroll.search(null, searchParameters); + scrollResult = channelScroll.query(searchParameters); result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertEquals(1000, result.size(), "Expected 1000 but got " + result.size()); @@ -122,20 +122,20 @@ void searchNameTest() throws InterruptedException { // Search for all 1000 SR channels and all 500 booster channels searchParameters.clear(); searchParameters.add("~name", "SR*|BR*"); - scrollResult = channelScroll.search(null, searchParameters); + scrollResult = channelScroll.query(searchParameters); result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertEquals(1500, result.size(), "Expected 1500 but got " + result.size()); searchParameters.clear(); searchParameters.add("~name", "SR*,BR*"); - scrollResult = channelScroll.search(null, searchParameters); + scrollResult = channelScroll.query(searchParameters); result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertEquals(1500, result.size(), "Expected 1500 but got " + result.size()); @@ -149,10 +149,10 @@ void searchNameTest() throws InterruptedException { searchParameters.add("~name", "SR*"); searchParameters.add("~tag", "group" + id + "_" + val_bucket.get(index)); - scrollResult = channelScroll.search(null, searchParameters); + scrollResult = channelScroll.query(searchParameters); result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertEquals( @@ -175,10 +175,10 @@ void searchNameTest() throws InterruptedException { searchParameters.add("~name", "SR*"); searchParameters.add("group" + id, String.valueOf(val_bucket.get(index))); - scrollResult = channelScroll.search(null, searchParameters); + scrollResult = channelScroll.query(searchParameters); result = scrollResult.getChannels(); while (scrollResult.getChannels().size() == 100) { - scrollResult = channelScroll.search(scrollResult.getId(), searchParameters); + scrollResult = channelScroll.query(scrollResult.getId(), searchParameters); result.addAll(scrollResult.getChannels()); } Assertions.assertEquals( diff --git a/src/test/java/org/phoebus/channelfinder/ChannelValidationIT.java b/src/test/java/org/phoebus/channelfinder/ChannelValidationIT.java deleted file mode 100644 index b15e096c..00000000 --- a/src/test/java/org/phoebus/channelfinder/ChannelValidationIT.java +++ /dev/null @@ -1,199 +0,0 @@ -package org.phoebus.channelfinder; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.IOException; -import java.util.Arrays; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.phoebus.channelfinder.configuration.ElasticConfig; -import org.phoebus.channelfinder.entity.Channel; -import org.phoebus.channelfinder.entity.Property; -import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.repository.ChannelRepository; -import org.phoebus.channelfinder.repository.PropertyRepository; -import org.phoebus.channelfinder.repository.TagRepository; -import org.phoebus.channelfinder.rest.api.IChannel; -import org.phoebus.channelfinder.rest.controller.ChannelController; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.web.server.ResponseStatusException; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@WebMvcTest(ChannelController.class) -@WithMockUser(roles = "CF-ADMINS") -@TestPropertySource(value = "classpath:application_test.properties") -@ContextConfiguration(classes = {ChannelController.class, ElasticConfig.class}) -class ChannelValidationIT { - - @Autowired IChannel channelManager; - - @Autowired TagRepository tagRepository; - - @Autowired PropertyRepository propertyRepository; - - @Autowired ChannelRepository channelRepository; - - @Autowired ElasticConfig esService; - - @BeforeAll - void setupAll() { - ElasticConfigIT.setUp(esService); - } - - @AfterAll - void tearDown() throws IOException { - ElasticConfigIT.teardown(esService); - } - - /** Attempt to Channel request with null name */ - @Test - void validateXmlChannelRequestNullName() { - Channel testChannel1 = new Channel(null, "testOwner"); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - } - - /** Attempt to Channel request with empty name */ - @Test - void validateXmlChannelRequestEmptyName() { - Channel testChannel1 = new Channel("", "testOwner"); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - } - - /** Attempt to Channel request with null owner */ - @Test - void validateXmlChannelRequestNullOwner() { - Channel testChannel1 = new Channel("testChannel1", null); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - } - - /** Attempt to Channel request with empty owner */ - @Test - void validateXmlChannelRequestEmptyOwner() { - Channel testChannel1 = new Channel("testChannel1", ""); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - } - - /** Attempt to Channel request with a non existent tag */ - @Test - void validateXmlChannelRequestFakeTag() { - // set up - Property prop = new Property("testProperty1", "testOwner"); - propertyRepository.index(prop); - prop.setValue("value"); - - Channel testChannel1 = - new Channel( - "testChannel1", - "testOwner", - Arrays.asList(prop), - Arrays.asList(new Tag("Non-existent-tag"))); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - - // clean up - propertyRepository.deleteById(prop.getName()); - } - - /** Attempt to Channel request with a non existent prop */ - @Test - void validateXmlChannelRequestFakeProp() { - // set up - Tag tag = new Tag("testTag1", "testOwner"); - tagRepository.index(tag); - - Channel testChannel1 = - new Channel( - "testChannel1", - "testOwner", - Arrays.asList(new Property("Non-existent-property", "Non-existent-property")), - Arrays.asList(tag)); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - - // clean up - tagRepository.deleteById(tag.getName()); - } - - /** Attempt to Channel request with a null value prop */ - @Test - void validateXmlChannelRequestNullProp() { - // set up - Tag tag = new Tag("testTag1", "testOwner"); - tagRepository.index(tag); - Property prop = new Property("testProperty1", "testOwner"); - propertyRepository.index(prop); - prop.setValue(null); - - Channel testChannel1 = - new Channel("testChannel1", "testOwner", Arrays.asList(prop), Arrays.asList(tag)); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - - // clean up - tagRepository.deleteById(tag.getName()); - propertyRepository.deleteById(prop.getName()); - } - - /** Attempt to Channel request with an empty value prop */ - @Test - void validateXmlChannelRequestEmptyProp() { - // set up - Tag tag = new Tag("testTag1", "testOwner"); - tagRepository.index(tag); - Property prop = new Property("testProperty1", "testOwner"); - propertyRepository.index(prop); - prop.setValue(""); - - Channel testChannel1 = - new Channel("testChannel1", "testOwner", Arrays.asList(prop), Arrays.asList(tag)); - assertThrows( - ResponseStatusException.class, () -> channelManager.validateChannelRequest(testChannel1)); - - // clean up - tagRepository.deleteById(tag.getName()); - propertyRepository.deleteById(prop.getName()); - } - - /** Attempt to Channel request with valid parameters */ - @Test - void validateXmlChannelRequest() { - Channel testChannel1 = new Channel("testChannel1", "testOwner"); - try { - channelManager.validateChannelRequest(testChannel1); - Assertions.assertTrue(true); - } catch (Exception e) { - fail("Failed to validate with valid parameters"); - } - } - - /** Attempt to Channel request with valid parameters */ - @Test - void validateXmlChannelRequest2() { - // set up - Tag tag = new Tag("testTag1", "testOwner"); - tagRepository.index(tag); - Property prop = new Property("testProperty1", "testOwner"); - propertyRepository.index(prop); - prop.setValue("value"); - - Channel testChannel1 = - new Channel("testChannel1", "testOwner", Arrays.asList(prop), Arrays.asList(tag)); - channelManager.validateChannelRequest(testChannel1); - - // clean up - tagRepository.deleteById(tag.getName()); - propertyRepository.deleteById(prop.getName()); - } -} diff --git a/src/test/java/org/phoebus/channelfinder/PropertyValidationIT.java b/src/test/java/org/phoebus/channelfinder/PropertyValidationIT.java deleted file mode 100644 index f86ac3eb..00000000 --- a/src/test/java/org/phoebus/channelfinder/PropertyValidationIT.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.phoebus.channelfinder; - -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.phoebus.channelfinder.configuration.ElasticConfig; -import org.phoebus.channelfinder.entity.Channel; -import org.phoebus.channelfinder.entity.Property; -import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.repository.ChannelRepository; -import org.phoebus.channelfinder.rest.api.IProperty; -import org.phoebus.channelfinder.rest.controller.PropertyController; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.web.server.ResponseStatusException; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@WebMvcTest(PropertyController.class) -@WithMockUser(roles = "CF-ADMINS") -@TestPropertySource(value = "classpath:application_test.properties") -@ContextConfiguration(classes = {PropertyController.class, ElasticConfig.class}) -class PropertyValidationIT { - - @Autowired IProperty propertyManager; - - @Autowired ChannelRepository channelRepository; - - @Autowired ElasticConfig esService; - - @AfterAll - void tearDown() throws IOException { - ElasticConfigIT.teardown(esService); - } - - /** Attempt to Property request with null name */ - @Test - void validateXmlPropertyRequestNullName() { - Property testProperty1 = new Property(null, "testOwner"); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - } - - /** Attempt to Property request with empty name */ - @Test - void validateXmlPropertyRequestEmptyName() { - Property testProperty1 = new Property("", "testOwner"); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - } - - /** Attempt to Property request with null owner */ - @Test - void validateXmlPropertyRequestNullOwner() { - Property testProperty1 = new Property("testProperty1", null); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - } - - /** Attempt to Property request with empty owner */ - @Test - void validateXmlPropertyRequestEmptyOwner() { - Property testProperty1 = new Property("testProperty1", ""); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - } - - /** Attempt to Property request with a non existent channel */ - @Test - void validateXmlPropertyRequestFakeChannel() { - Property testProperty1 = new Property("testProperty1", "testOwner"); - testProperty1.setChannels(Arrays.asList(new Channel("Non-existent-channel"))); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - } - - /** Attempt to Property request with multiple non existent channels */ - @Test - void validateXmlPropertyRequestFakeChannels() { - Property testProperty1 = new Property("testProperty1", "testOwner"); - testProperty1.setChannels( - Arrays.asList(new Channel("Non-existent-channel"), new Channel("Non-existent-channel"))); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - } - - /** Attempt to Property request with some existent(and valid) and some non existent channels */ - @Test - void validateXmlPropertyRequestSomeFakeChannels() { - Channel chan = new Channel("testChannel0", "testOwner"); - channelRepository.index(chan); - Property testProperty1 = new Property("testProperty1", "testOwner1"); - testProperty1.setChannels( - Arrays.asList( - new Channel( - chan.getName(), - chan.getOwner(), - Arrays.asList( - new Property(testProperty1.getName(), testProperty1.getOwner(), "value")), - new ArrayList()), - new Channel("Non-existent-channel"))); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - channelRepository.deleteById("testChannel0"); - } - - /** Attempt to Property request with a channel that has no prop */ - @Test - void validateXmlPropertyRequestNoProp() { - Channel chan = new Channel("testChannel0", "testOwner"); - channelRepository.index(chan); - Property testProperty1 = new Property("testProperty1", "testOwner1"); - testProperty1.setChannels( - Arrays.asList( - new Channel( - chan.getName(), chan.getOwner(), new ArrayList(), new ArrayList()))); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - channelRepository.deleteById("testChannel0"); - } - - /** Attempt to Property request with a null value */ - @Test - void validateXmlPropertyRequestNullValue() { - Channel chan = new Channel("testChannel0", "testOwner"); - channelRepository.index(chan); - Property testProperty1 = new Property("testProperty1", "testOwner1"); - testProperty1.setChannels( - Arrays.asList( - new Channel( - chan.getName(), - chan.getOwner(), - Arrays.asList( - new Property(testProperty1.getName(), testProperty1.getOwner(), null)), - new ArrayList()))); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - channelRepository.deleteById("testChannel0"); - } - - /** Attempt to Property request with an empty value */ - @Test - void validateXmlPropertyRequestEmptyValue() { - Channel chan = new Channel("testChannel0", "testOwner"); - channelRepository.index(chan); - Property testProperty1 = new Property("testProperty1", "testOwner1"); - testProperty1.setChannels( - Arrays.asList( - new Channel( - chan.getName(), - chan.getOwner(), - Arrays.asList(new Property(testProperty1.getName(), testProperty1.getOwner(), "")), - new ArrayList()))); - Assertions.assertThrows( - ResponseStatusException.class, - () -> propertyManager.validatePropertyRequest(testProperty1)); - channelRepository.deleteById("testChannel0"); - } - - /** Attempt to Property request with valid parameters */ - @Test - void validateXmlPropertyRequest() { - Channel chan = new Channel("testChannel0", "testOwner"); - channelRepository.index(chan); - Property testProperty1 = new Property("testProperty1", "testOwner1"); - testProperty1.setChannels( - Arrays.asList( - new Channel( - chan.getName(), - chan.getOwner(), - Arrays.asList( - new Property(testProperty1.getName(), testProperty1.getOwner(), "value")), - new ArrayList()))); - try { - propertyManager.validatePropertyRequest(testProperty1); - Assertions.assertTrue(true); - } catch (Exception e) { - fail("Failed to validate with valid parameters"); - } - channelRepository.deleteById("testChannel0"); - } - - /** Attempt to Property request with other valid parameters */ - @Test - void validateXmlPropertyRequest2() { - Property testProperty1 = new Property("testProperty1", "testOwner1"); - try { - propertyManager.validatePropertyRequest(testProperty1); - Assertions.assertTrue(true); - } catch (Exception e) { - fail("Failed to validate with valid parameters"); - } - } -} diff --git a/src/test/java/org/phoebus/channelfinder/TagValidationIT.java b/src/test/java/org/phoebus/channelfinder/TagValidationIT.java deleted file mode 100644 index 4baecdf3..00000000 --- a/src/test/java/org/phoebus/channelfinder/TagValidationIT.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.phoebus.channelfinder; - -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.phoebus.channelfinder.configuration.ElasticConfig; -import org.phoebus.channelfinder.entity.Channel; -import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.repository.ChannelRepository; -import org.phoebus.channelfinder.rest.api.ITag; -import org.phoebus.channelfinder.rest.controller.TagController; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.web.server.ResponseStatusException; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@WebMvcTest(TagController.class) -@WithMockUser(roles = "CF-ADMINS") -@TestPropertySource(value = "classpath:application_test.properties") -@ContextConfiguration(classes = {TagController.class, ElasticConfig.class}) -class TagValidationIT { - - @Autowired ITag tagManager; - - @Autowired ElasticConfig esService; - @Autowired ChannelRepository channelRepository; - - /** Attempt to Tag request with null name */ - @Test - void validateXmlTagRequestNullName() { - Tag testTag1 = new Tag(null, "testOwner"); - Assertions.assertThrows( - ResponseStatusException.class, () -> tagManager.validateTagRequest(testTag1)); - } - - /** Attempt to Tag request with empty name */ - @Test - void validateXmlTagRequestEmptyName() { - Tag testTag1 = new Tag("", "testOwner"); - Assertions.assertThrows( - ResponseStatusException.class, () -> tagManager.validateTagRequest(testTag1)); - } - - /** Attempt to Tag request with null owner */ - @Test - void validateXmlTagRequestNullOwner() { - Tag testTag1 = new Tag("testTag1", null); - Assertions.assertThrows( - ResponseStatusException.class, () -> tagManager.validateTagRequest(testTag1)); - } - - /** Attempt to Tag request with empty owner */ - @Test - void validateXmlTagRequestEmptyOwner() { - Tag testTag1 = new Tag("testTag1", ""); - Assertions.assertThrows( - ResponseStatusException.class, () -> tagManager.validateTagRequest(testTag1)); - } - - /** Attempt to Tag request with a non existent channel */ - @Test - void validateXmlTagRequestFakeChannel() { - Tag testTag1 = new Tag("testTag1", "testOwner"); - testTag1.setChannels(Arrays.asList(new Channel("Non-existent-channel"))); - Assertions.assertThrows( - ResponseStatusException.class, () -> tagManager.validateTagRequest(testTag1)); - } - - /** Attempt to Tag request with multiple non existent channels */ - @Test - void validateXmlTagRequestFakeChannels() { - Tag testTag1 = new Tag("testTag1", "testOwner"); - testTag1.setChannels( - Arrays.asList(new Channel("Non-existent-channel"), new Channel("Non-existent-channel"))); - Assertions.assertThrows( - ResponseStatusException.class, () -> tagManager.validateTagRequest(testTag1)); - } - - /** Attempt to Tag request with some existent and some non existent channels */ - @Test - void validateXmlTagRequestSomeFakeChannels() { - channelRepository.indexAll(Arrays.asList(new Channel("testChannel0", "testOwner"))); - Tag testTag1 = new Tag("testTag1", "testOwner"); - testTag1.setChannels( - Arrays.asList(new Channel("Non-existent-channel"), new Channel("testChannel0"))); - Assertions.assertThrows( - ResponseStatusException.class, () -> tagManager.validateTagRequest(testTag1)); - } - - /** Attempt to Tag request with valid parameters */ - @Test - void validateXmlTagRequest() { - Tag testTag1 = new Tag("testTag1", "testOwner"); - try { - tagManager.validateTagRequest(testTag1); - Assertions.assertTrue(true); - } catch (Exception e) { - fail("Failed to validate with valid parameters"); - } - } - - /** Attempt to Tag request with other valid parameters */ - @Test - void validateXmlTagRequest2() { - channelRepository.indexAll(List.of(new Channel("testChannel0", "testOwner"))); - - Tag testTag1 = new Tag("testTag1", "testOwner"); - testTag1.setChannels(List.of(new Channel("testChannel0"))); - try { - tagManager.validateTagRequest(testTag1); - Assertions.assertTrue(true); - } catch (Exception e) { - fail("Failed to validate with valid parameters"); - } - - channelRepository.deleteById("testChannel0"); - } - - @AfterAll - void tearDown() throws IOException { - ElasticConfigIT.teardown(esService); - } -} diff --git a/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorServiceTest.java b/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorServiceTest.java index bbd47956..e3601542 100644 --- a/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorServiceTest.java +++ b/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorServiceTest.java @@ -71,7 +71,7 @@ public void reset() { @BeforeEach void setUp() { channelProcessorService = - new ChannelProcessorService(List.of(dummyProcessor), Runnable::run, 10); + new ChannelProcessorService(List.of(dummyProcessor), Runnable::run, null, null, 10, 10000); } @Test diff --git a/src/test/java/org/phoebus/channelfinder/service/ChannelServiceTest.java b/src/test/java/org/phoebus/channelfinder/service/ChannelServiceTest.java new file mode 100644 index 00000000..5d49034d --- /dev/null +++ b/src/test/java/org/phoebus/channelfinder/service/ChannelServiceTest.java @@ -0,0 +1,141 @@ +package org.phoebus.channelfinder.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.phoebus.channelfinder.entity.Channel; +import org.phoebus.channelfinder.entity.Property; +import org.phoebus.channelfinder.entity.Tag; +import org.phoebus.channelfinder.exceptions.ChannelValidationException; +import org.phoebus.channelfinder.exceptions.PropertyNotFoundException; +import org.phoebus.channelfinder.exceptions.TagNotFoundException; +import org.phoebus.channelfinder.repository.ChannelRepository; +import org.phoebus.channelfinder.repository.PropertyRepository; +import org.phoebus.channelfinder.repository.TagRepository; +import org.phoebus.channelfinder.service.AuthorizationService.ROLES; + +@ExtendWith(MockitoExtension.class) +class ChannelServiceTest { + + @Mock private ChannelRepository channelRepository; + @Mock private TagRepository tagRepository; + @Mock private PropertyRepository propertyRepository; + @Mock private AuthorizationService authorizationService; + @Mock private ChannelProcessorService channelProcessorService; + + private ChannelService channelService; + + @BeforeEach + void setUp() { + channelService = + new ChannelService( + channelRepository, + tagRepository, + propertyRepository, + authorizationService, + channelProcessorService); + when(authorizationService.isAuthorizedRole(any(), eq(ROLES.CF_CHANNEL))).thenReturn(true); + } + + @Test + void createChannel_nullName_throwsValidationException() { + Channel channel = new Channel(null, "owner"); + + assertThrows(ChannelValidationException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_emptyName_throwsValidationException() { + Channel channel = new Channel("", "owner"); + + assertThrows(ChannelValidationException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_nullOwner_throwsValidationException() { + Channel channel = new Channel("ch", null); + + assertThrows(ChannelValidationException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_emptyOwner_throwsValidationException() { + Channel channel = new Channel("ch", ""); + + assertThrows(ChannelValidationException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_nonExistentTag_throwsTagNotFoundException() { + Tag tag = new Tag("missing-tag", "owner"); + Channel channel = new Channel("ch", "owner", List.of(), List.of(tag)); + when(tagRepository.existsById("missing-tag")).thenReturn(false); + + assertThrows(TagNotFoundException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_nonExistentProperty_throwsPropertyNotFoundException() { + Property prop = new Property("missing-prop", "owner", "val"); + Channel channel = new Channel("ch", "owner", List.of(prop), List.of()); + when(propertyRepository.existsById("missing-prop")).thenReturn(false); + + assertThrows(PropertyNotFoundException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_nullPropertyValue_throwsValidationException() { + Property prop = new Property("prop1", "owner", null); + Channel channel = new Channel("ch", "owner", List.of(prop), List.of()); + when(propertyRepository.existsById("prop1")).thenReturn(true); + + assertThrows(ChannelValidationException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_emptyPropertyValue_throwsValidationException() { + Property prop = new Property("prop1", "owner", ""); + Channel channel = new Channel("ch", "owner", List.of(prop), List.of()); + when(propertyRepository.existsById("prop1")).thenReturn(true); + + assertThrows(ChannelValidationException.class, () -> channelService.create("ch", channel)); + } + + @Test + void createChannel_validChannel_noException() { + Channel channel = new Channel("ch", "owner"); + when(authorizationService.isAuthorizedOwner(any(), any(Channel.class))).thenReturn(true); + when(channelRepository.findById("ch")).thenReturn(Optional.empty()); + when(channelRepository.index(any())).thenReturn(channel); + when(propertyRepository.findAll()).thenReturn(List.of()); + when(tagRepository.findAll()).thenReturn(List.of()); + + assertDoesNotThrow(() -> channelService.create("ch", channel)); + } + + @Test + void createChannel_validChannelWithTagAndProperty_noException() { + Tag tag = new Tag("tag1", "owner"); + Property prop = new Property("prop1", "owner", "value"); + Channel channel = new Channel("ch", "owner", List.of(prop), List.of(tag)); + when(authorizationService.isAuthorizedOwner(any(), any(Channel.class))).thenReturn(true); + when(tagRepository.existsById("tag1")).thenReturn(true); + when(propertyRepository.existsById("prop1")).thenReturn(true); + when(channelRepository.findById("ch")).thenReturn(Optional.empty()); + when(channelRepository.index(any())).thenReturn(channel); + when(propertyRepository.findAll()).thenReturn(List.of(prop)); + when(tagRepository.findAll()).thenReturn(List.of(tag)); + + assertDoesNotThrow(() -> channelService.create("ch", channel)); + } +} diff --git a/src/test/java/org/phoebus/channelfinder/service/PropertyServiceTest.java b/src/test/java/org/phoebus/channelfinder/service/PropertyServiceTest.java new file mode 100644 index 00000000..8ba86d35 --- /dev/null +++ b/src/test/java/org/phoebus/channelfinder/service/PropertyServiceTest.java @@ -0,0 +1,140 @@ +package org.phoebus.channelfinder.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.phoebus.channelfinder.entity.Channel; +import org.phoebus.channelfinder.entity.Property; +import org.phoebus.channelfinder.exceptions.PropertyValidationException; +import org.phoebus.channelfinder.repository.ChannelRepository; +import org.phoebus.channelfinder.repository.PropertyRepository; +import org.phoebus.channelfinder.service.AuthorizationService.ROLES; + +@ExtendWith(MockitoExtension.class) +class PropertyServiceTest { + + @Mock private PropertyRepository propertyRepository; + @Mock private ChannelRepository channelRepository; + @Mock private AuthorizationService authorizationService; + + private PropertyService propertyService; + + @BeforeEach + void setUp() { + propertyService = + new PropertyService(propertyRepository, channelRepository, authorizationService); + when(authorizationService.isAuthorizedRole(any(), eq(ROLES.CF_PROPERTY))).thenReturn(true); + } + + @Test + void createProperty_nullName_throwsValidationException() { + Property property = new Property(null, "owner"); + + assertThrows(PropertyValidationException.class, () -> propertyService.create("prop", property)); + } + + @Test + void createProperty_emptyName_throwsValidationException() { + Property property = new Property("", "owner"); + + assertThrows(PropertyValidationException.class, () -> propertyService.create("prop", property)); + } + + @Test + void createProperty_nullOwner_throwsValidationException() { + Property property = new Property("prop1", null); + + assertThrows( + PropertyValidationException.class, () -> propertyService.create("prop1", property)); + } + + @Test + void createProperty_emptyOwner_throwsValidationException() { + Property property = new Property("prop1", ""); + + assertThrows( + PropertyValidationException.class, () -> propertyService.create("prop1", property)); + } + + @Test + void createProperty_nonExistentChannel_throwsValidationException() { + Property property = new Property("prop1", "owner"); + Channel channel = new Channel("missing-channel", "owner", List.of(), List.of()); + channel.addProperty(new Property("prop1", "owner", "value")); + property.setChannels(List.of(channel)); + when(channelRepository.existsById("missing-channel")).thenReturn(false); + + assertThrows( + PropertyValidationException.class, () -> propertyService.create("prop1", property)); + } + + @Test + void createProperty_channelMissingPropertyValue_throwsValidationException() { + Property property = new Property("prop1", "owner"); + Channel channel = new Channel("ch1", "owner", List.of(), List.of()); + property.setChannels(List.of(channel)); + when(channelRepository.existsById("ch1")).thenReturn(true); + + assertThrows( + PropertyValidationException.class, () -> propertyService.create("prop1", property)); + } + + @Test + void createProperty_channelWithNullPropertyValue_throwsValidationException() { + Property property = new Property("prop1", "owner"); + Channel channel = + new Channel("ch1", "owner", List.of(new Property("prop1", "owner", null)), List.of()); + property.setChannels(List.of(channel)); + when(channelRepository.existsById("ch1")).thenReturn(true); + + assertThrows( + PropertyValidationException.class, () -> propertyService.create("prop1", property)); + } + + @Test + void createProperty_channelWithEmptyPropertyValue_throwsValidationException() { + Property property = new Property("prop1", "owner"); + Channel channel = + new Channel("ch1", "owner", List.of(new Property("prop1", "owner", "")), List.of()); + property.setChannels(List.of(channel)); + when(channelRepository.existsById("ch1")).thenReturn(true); + + assertThrows( + PropertyValidationException.class, () -> propertyService.create("prop1", property)); + } + + @Test + void createProperty_validPropertyNoChannels_noException() { + Property property = new Property("prop1", "owner"); + when(authorizationService.isAuthorizedOwner(any(), any(Property.class))).thenReturn(true); + when(propertyRepository.findById("prop1")).thenReturn(Optional.empty()); + when(propertyRepository.index(any())).thenReturn(property); + + assertDoesNotThrow(() -> propertyService.create("prop1", property)); + } + + @Test + void createProperty_validPropertyWithChannel_noException() { + Property property = new Property("prop1", "owner"); + Channel channel = + new Channel("ch1", "owner", List.of(new Property("prop1", "owner", "value")), List.of()); + property.setChannels(List.of(channel)); + when(authorizationService.isAuthorizedOwner(any(), any(Property.class))).thenReturn(true); + when(channelRepository.existsById("ch1")).thenReturn(true); + when(propertyRepository.findById("prop1")).thenReturn(Optional.empty()); + when(propertyRepository.index(any())).thenReturn(property); + when(channelRepository.saveAll(any())).thenReturn(List.of(channel)); + + assertDoesNotThrow(() -> propertyService.create("prop1", property)); + } +} diff --git a/src/test/java/org/phoebus/channelfinder/service/TagServiceTest.java b/src/test/java/org/phoebus/channelfinder/service/TagServiceTest.java new file mode 100644 index 00000000..92d2e21a --- /dev/null +++ b/src/test/java/org/phoebus/channelfinder/service/TagServiceTest.java @@ -0,0 +1,99 @@ +package org.phoebus.channelfinder.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.phoebus.channelfinder.entity.Channel; +import org.phoebus.channelfinder.entity.Tag; +import org.phoebus.channelfinder.exceptions.ChannelNotFoundException; +import org.phoebus.channelfinder.exceptions.TagValidationException; +import org.phoebus.channelfinder.repository.ChannelRepository; +import org.phoebus.channelfinder.repository.TagRepository; +import org.phoebus.channelfinder.service.AuthorizationService.ROLES; + +@ExtendWith(MockitoExtension.class) +class TagServiceTest { + + @Mock private TagRepository tagRepository; + @Mock private ChannelRepository channelRepository; + @Mock private AuthorizationService authorizationService; + + private TagService tagService; + + @BeforeEach + void setUp() { + tagService = new TagService(tagRepository, channelRepository, authorizationService); + when(authorizationService.isAuthorizedRole(any(), eq(ROLES.CF_TAG))).thenReturn(true); + } + + @Test + void createTag_nullName_throwsValidationException() { + Tag tag = new Tag(null, "owner"); + + assertThrows(TagValidationException.class, () -> tagService.create("tag", tag)); + } + + @Test + void createTag_emptyName_throwsValidationException() { + Tag tag = new Tag("", "owner"); + + assertThrows(TagValidationException.class, () -> tagService.create("tag", tag)); + } + + @Test + void createTag_nullOwner_throwsValidationException() { + Tag tag = new Tag("tag1", null); + + assertThrows(TagValidationException.class, () -> tagService.create("tag1", tag)); + } + + @Test + void createTag_emptyOwner_throwsValidationException() { + Tag tag = new Tag("tag1", ""); + + assertThrows(TagValidationException.class, () -> tagService.create("tag1", tag)); + } + + @Test + void createTag_nonExistentChannel_throwsChannelNotFoundException() { + Tag tag = new Tag("tag1", "owner"); + tag.setChannels(List.of(new Channel("missing-channel"))); + when(channelRepository.existsById("missing-channel")).thenReturn(false); + + assertThrows(ChannelNotFoundException.class, () -> tagService.create("tag1", tag)); + } + + @Test + void createTag_validTag_noException() { + Tag tag = new Tag("tag1", "owner"); + when(authorizationService.isAuthorizedOwner(any(), any(Tag.class))).thenReturn(true); + when(tagRepository.findById("tag1")).thenReturn(Optional.empty()); + when(tagRepository.index(any())).thenReturn(tag); + + assertDoesNotThrow(() -> tagService.create("tag1", tag)); + } + + @Test + void createTag_validTagWithExistingChannel_noException() { + Channel channel = new Channel("ch1", "owner"); + Tag tag = new Tag("tag1", "owner"); + tag.setChannels(List.of(channel)); + when(authorizationService.isAuthorizedOwner(any(), any(Tag.class))).thenReturn(true); + when(channelRepository.existsById("ch1")).thenReturn(true); + when(tagRepository.findById("tag1")).thenReturn(Optional.empty()); + when(tagRepository.index(any())).thenReturn(tag); + when(channelRepository.saveAll(any())).thenReturn(List.of(channel)); + + assertDoesNotThrow(() -> tagService.create("tag1", tag)); + } +} From 6b12092ad39181fa44820b7cae3639fa3087e4a4 Mon Sep 17 00:00:00 2001 From: Anders Lindh Olsson Date: Thu, 2 Apr 2026 11:26:12 +0200 Subject: [PATCH 4/6] refactor(web): move legacy API to web.legacy package Introduce a web package hierarchy that makes the structural intent clear and leaves room for a future web.v1 package alongside it. - rest.api.* -> web.legacy.api.* - rest.controller.* -> web.legacy.controller.* - ChannelFinderExceptionHandler -> web (cross-cutting, not legacy-specific) Update all test imports accordingly. No behaviour change. --- .../ChannelFinderExceptionHandler.java | 2 +- .../{rest => web/legacy}/api/IChannel.java | 2 +- .../legacy}/api/IChannelProcessor.java | 2 +- .../{rest => web/legacy}/api/IChannelScroll.java | 2 +- .../{rest => web/legacy}/api/IInfo.java | 2 +- .../{rest => web/legacy}/api/IProperty.java | 2 +- .../{rest => web/legacy}/api/ITag.java | 2 +- .../legacy}/controller/ChannelController.java | 6 ++---- .../controller/ChannelProcessorController.java | 4 ++-- .../controller/ChannelScrollController.java | 6 ++---- .../legacy}/controller/InfoController.java | 6 ++---- .../legacy}/controller/PropertyController.java | 6 ++---- .../legacy}/controller/TagController.java | 6 ++---- .../phoebus/channelfinder/ChannelControllerIT.java | 8 ++++---- .../channelfinder/ChannelRepositorySearchIT.java | 5 +++++ .../channelfinder/ChannelScrollControllerIT.java | 14 +++++++++----- .../ChannelScrollControllerSearchIT.java | 4 ++-- .../channelfinder/PropertyControllerIT.java | 10 +++++----- .../org/phoebus/channelfinder/TagControllerIT.java | 10 +++++----- .../docker/ChannelFinderChannelsIT.java | 2 +- .../docker/ChannelFinderPropertiesIT.java | 2 +- .../docker/ChannelFinderScrollIT.java | 2 +- .../channelfinder/docker/ChannelFinderTagsIT.java | 2 +- .../channelfinder/epics/EpicsRPCRequestIT.java | 6 +++--- .../processors/ChannelProcessorControllerIT.java | 13 +++++++------ 25 files changed, 63 insertions(+), 63 deletions(-) rename src/main/java/org/phoebus/channelfinder/{rest/controller => web}/ChannelFinderExceptionHandler.java (98%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/api/IChannel.java (99%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/api/IChannelProcessor.java (98%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/api/IChannelScroll.java (98%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/api/IInfo.java (94%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/api/IProperty.java (99%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/api/ITag.java (99%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/controller/ChannelController.java (91%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/controller/ChannelProcessorController.java (93%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/controller/ChannelScrollController.java (85%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/controller/InfoController.java (78%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/controller/PropertyController.java (91%) rename src/main/java/org/phoebus/channelfinder/{rest => web/legacy}/controller/TagController.java (90%) diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelFinderExceptionHandler.java b/src/main/java/org/phoebus/channelfinder/web/ChannelFinderExceptionHandler.java similarity index 98% rename from src/main/java/org/phoebus/channelfinder/rest/controller/ChannelFinderExceptionHandler.java rename to src/main/java/org/phoebus/channelfinder/web/ChannelFinderExceptionHandler.java index ab846891..75cb0e22 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelFinderExceptionHandler.java +++ b/src/main/java/org/phoebus/channelfinder/web/ChannelFinderExceptionHandler.java @@ -1,4 +1,4 @@ -package org.phoebus.channelfinder.rest.controller; +package org.phoebus.channelfinder.web; import java.util.logging.Level; import java.util.logging.Logger; diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IChannel.java b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannel.java similarity index 99% rename from src/main/java/org/phoebus/channelfinder/rest/api/IChannel.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannel.java index d947d182..9145b58a 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IChannel.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannel.java @@ -1,4 +1,4 @@ -package org.phoebus.channelfinder.rest.api; +package org.phoebus.channelfinder.web.legacy.api; import static org.phoebus.channelfinder.common.CFResourceDescriptors.SEARCH_PARAM_DESCRIPTION; diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelProcessor.java b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannelProcessor.java similarity index 98% rename from src/main/java/org/phoebus/channelfinder/rest/api/IChannelProcessor.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannelProcessor.java index bde4ea2d..ccf40cf9 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelProcessor.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannelProcessor.java @@ -1,4 +1,4 @@ -package org.phoebus.channelfinder.rest.api; +package org.phoebus.channelfinder.web.legacy.api; import static org.phoebus.channelfinder.common.CFResourceDescriptors.SEARCH_PARAM_DESCRIPTION; diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannelScroll.java similarity index 98% rename from src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannelScroll.java index c98b9ed1..8c9a37c0 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IChannelScroll.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IChannelScroll.java @@ -1,4 +1,4 @@ -package org.phoebus.channelfinder.rest.api; +package org.phoebus.channelfinder.web.legacy.api; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IInfo.java b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IInfo.java similarity index 94% rename from src/main/java/org/phoebus/channelfinder/rest/api/IInfo.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/api/IInfo.java index 8d19310a..9a1a3bf2 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IInfo.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IInfo.java @@ -1,4 +1,4 @@ -package org.phoebus.channelfinder.rest.api; +package org.phoebus.channelfinder.web.legacy.api; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/IProperty.java b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IProperty.java similarity index 99% rename from src/main/java/org/phoebus/channelfinder/rest/api/IProperty.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/api/IProperty.java index f05a37e4..13af0503 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/IProperty.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/api/IProperty.java @@ -1,4 +1,4 @@ -package org.phoebus.channelfinder.rest.api; +package org.phoebus.channelfinder.web.legacy.api; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; diff --git a/src/main/java/org/phoebus/channelfinder/rest/api/ITag.java b/src/main/java/org/phoebus/channelfinder/web/legacy/api/ITag.java similarity index 99% rename from src/main/java/org/phoebus/channelfinder/rest/api/ITag.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/api/ITag.java index a525f1c3..2ba97b2f 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/api/ITag.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/api/ITag.java @@ -1,4 +1,4 @@ -package org.phoebus.channelfinder.rest.api; +package org.phoebus.channelfinder.web.legacy.api; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelController.java b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelController.java similarity index 91% rename from src/main/java/org/phoebus/channelfinder/rest/controller/ChannelController.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelController.java index 03563c8d..7a5af741 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelController.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelController.java @@ -1,17 +1,15 @@ -package org.phoebus.channelfinder.rest.controller; +package org.phoebus.channelfinder.web.legacy.controller; import java.util.List; import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.SearchResult; -import org.phoebus.channelfinder.rest.api.IChannel; import org.phoebus.channelfinder.service.ChannelService; +import org.phoebus.channelfinder.web.legacy.api.IChannel; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.util.MultiValueMap; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@CrossOrigin @RestController @EnableAutoConfiguration @RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/channels") diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelProcessorController.java b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelProcessorController.java similarity index 93% rename from src/main/java/org/phoebus/channelfinder/rest/controller/ChannelProcessorController.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelProcessorController.java index 1e40e47e..b5feadde 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelProcessorController.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelProcessorController.java @@ -1,10 +1,10 @@ -package org.phoebus.channelfinder.rest.controller; +package org.phoebus.channelfinder.web.legacy.controller; import java.util.List; import org.phoebus.channelfinder.entity.Channel; -import org.phoebus.channelfinder.rest.api.IChannelProcessor; import org.phoebus.channelfinder.service.ChannelProcessorService; import org.phoebus.channelfinder.service.model.archiver.ChannelProcessorInfo; +import org.phoebus.channelfinder.web.legacy.api.IChannelProcessor; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelScrollController.java b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelScrollController.java similarity index 85% rename from src/main/java/org/phoebus/channelfinder/rest/controller/ChannelScrollController.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelScrollController.java index 7e2fc6e4..f4e6fd73 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/ChannelScrollController.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/ChannelScrollController.java @@ -1,15 +1,13 @@ -package org.phoebus.channelfinder.rest.controller; +package org.phoebus.channelfinder.web.legacy.controller; import org.phoebus.channelfinder.entity.Scroll; -import org.phoebus.channelfinder.rest.api.IChannelScroll; import org.phoebus.channelfinder.service.ChannelScrollService; +import org.phoebus.channelfinder.web.legacy.api.IChannelScroll; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.util.MultiValueMap; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@CrossOrigin @RestController @EnableAutoConfiguration @RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/scroll") diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/InfoController.java b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/InfoController.java similarity index 78% rename from src/main/java/org/phoebus/channelfinder/rest/controller/InfoController.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/controller/InfoController.java index 89024a43..05a923a6 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/InfoController.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/InfoController.java @@ -1,13 +1,11 @@ -package org.phoebus.channelfinder.rest.controller; +package org.phoebus.channelfinder.web.legacy.controller; -import org.phoebus.channelfinder.rest.api.IInfo; import org.phoebus.channelfinder.service.InfoService; +import org.phoebus.channelfinder.web.legacy.api.IInfo; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@CrossOrigin @RestController @EnableAutoConfiguration @RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}") diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/PropertyController.java b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/PropertyController.java similarity index 91% rename from src/main/java/org/phoebus/channelfinder/rest/controller/PropertyController.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/controller/PropertyController.java index 67f92aa7..f29396f9 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/PropertyController.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/PropertyController.java @@ -1,14 +1,12 @@ -package org.phoebus.channelfinder.rest.controller; +package org.phoebus.channelfinder.web.legacy.controller; import org.phoebus.channelfinder.entity.Property; -import org.phoebus.channelfinder.rest.api.IProperty; import org.phoebus.channelfinder.service.PropertyService; +import org.phoebus.channelfinder.web.legacy.api.IProperty; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@CrossOrigin @RestController @EnableAutoConfiguration @RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/properties") diff --git a/src/main/java/org/phoebus/channelfinder/rest/controller/TagController.java b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/TagController.java similarity index 90% rename from src/main/java/org/phoebus/channelfinder/rest/controller/TagController.java rename to src/main/java/org/phoebus/channelfinder/web/legacy/controller/TagController.java index 5bbb6ad3..0d6e9d69 100644 --- a/src/main/java/org/phoebus/channelfinder/rest/controller/TagController.java +++ b/src/main/java/org/phoebus/channelfinder/web/legacy/controller/TagController.java @@ -1,14 +1,12 @@ -package org.phoebus.channelfinder.rest.controller; +package org.phoebus.channelfinder.web.legacy.controller; import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.rest.api.ITag; import org.phoebus.channelfinder.service.TagService; +import org.phoebus.channelfinder.web.legacy.api.ITag; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@CrossOrigin @RestController @EnableAutoConfiguration @RequestMapping("${channelfinder.legacy.service-root:ChannelFinder}/resources/tags") diff --git a/src/test/java/org/phoebus/channelfinder/ChannelControllerIT.java b/src/test/java/org/phoebus/channelfinder/ChannelControllerIT.java index ad2b3383..74a82756 100644 --- a/src/test/java/org/phoebus/channelfinder/ChannelControllerIT.java +++ b/src/test/java/org/phoebus/channelfinder/ChannelControllerIT.java @@ -17,17 +17,17 @@ import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Property; import org.phoebus.channelfinder.entity.Tag; +import org.phoebus.channelfinder.exceptions.ChannelNotFoundException; import org.phoebus.channelfinder.repository.ChannelRepository; import org.phoebus.channelfinder.repository.PropertyRepository; import org.phoebus.channelfinder.repository.TagRepository; -import org.phoebus.channelfinder.rest.api.IChannel; -import org.phoebus.channelfinder.rest.controller.ChannelController; +import org.phoebus.channelfinder.web.legacy.api.IChannel; +import org.phoebus.channelfinder.web.legacy.controller.ChannelController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; -import org.springframework.web.server.ResponseStatusException; @TestInstance(TestInstance.Lifecycle.PER_CLASS) @WebMvcTest(ChannelController.class) @@ -64,7 +64,7 @@ void readXmlChannel() { void readNonExistingXmlChannel() { // verify the channel failed to be read, as expected Assertions.assertThrows( - ResponseStatusException.class, () -> channelManager.read("fakeChannel")); + ChannelNotFoundException.class, () -> channelManager.read("fakeChannel")); } /** create a simple channel */ diff --git a/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java b/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java index 66e041b1..7b887e6e 100644 --- a/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java +++ b/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.phoebus.channelfinder.configuration.ElasticConfig; import org.phoebus.channelfinder.configuration.PopulateDBConfiguration; import org.phoebus.channelfinder.entity.SearchResult; @@ -33,6 +34,10 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) @WebMvcTest(ChannelRepository.class) @TestPropertySource(locations = "classpath:application_test.properties") +@EnabledIfEnvironmentVariable( + named = "GITHUB_ACTIONS", + matches = "true", + disabledReason = "Requires Elasticsearch on localhost:9200; runs in CI only") @ContextConfiguration(classes = {ChannelRepository.class, ElasticConfig.class}) class ChannelRepositorySearchIT { private static final Logger logger = Logger.getLogger(ChannelRepositorySearchIT.class.getName()); diff --git a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java index ea45f02d..3164d5b9 100644 --- a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java +++ b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerIT.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Random; import org.junit.jupiter.api.*; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.phoebus.channelfinder.configuration.ElasticConfig; import org.phoebus.channelfinder.configuration.PopulateDBConfiguration; import org.phoebus.channelfinder.entity.Channel; @@ -12,8 +13,8 @@ import org.phoebus.channelfinder.repository.ChannelRepository; import org.phoebus.channelfinder.repository.PropertyRepository; import org.phoebus.channelfinder.repository.TagRepository; -import org.phoebus.channelfinder.rest.api.IChannelScroll; -import org.phoebus.channelfinder.rest.controller.ChannelScrollController; +import org.phoebus.channelfinder.web.legacy.api.IChannelScroll; +import org.phoebus.channelfinder.web.legacy.controller.ChannelScrollController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; @@ -25,6 +26,10 @@ @WebMvcTest(ChannelScrollController.class) @TestPropertySource(value = "classpath:application_test.properties") @ContextConfiguration(classes = {ChannelScrollController.class, ElasticConfig.class}) +@EnabledIfEnvironmentVariable( + named = "GITHUB_ACTIONS", + matches = "true", + disabledReason = "Requires Elasticsearch on localhost:9200; runs in CI only") class ChannelScrollControllerIT { @Autowired IChannelScroll channelScroll; @@ -40,13 +45,12 @@ class ChannelScrollControllerIT { @Autowired PopulateDBConfiguration populateDBConfiguration; @BeforeEach - public void setup() throws InterruptedException { + void setup() throws IOException { populateDBConfiguration.createDB(1); - Thread.sleep(10000); } @AfterEach - public void cleanup() throws InterruptedException { + void cleanup() { populateDBConfiguration.cleanupDB(); } diff --git a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java index 6cb7dadc..0d5a4e29 100644 --- a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java +++ b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java @@ -14,8 +14,8 @@ import org.phoebus.channelfinder.entity.Scroll; import org.phoebus.channelfinder.repository.PropertyRepository; import org.phoebus.channelfinder.repository.TagRepository; -import org.phoebus.channelfinder.rest.api.IChannelScroll; -import org.phoebus.channelfinder.rest.controller.ChannelScrollController; +import org.phoebus.channelfinder.web.legacy.api.IChannelScroll; +import org.phoebus.channelfinder.web.legacy.controller.ChannelScrollController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ContextConfiguration; diff --git a/src/test/java/org/phoebus/channelfinder/PropertyControllerIT.java b/src/test/java/org/phoebus/channelfinder/PropertyControllerIT.java index 26972c3a..d8d1840d 100644 --- a/src/test/java/org/phoebus/channelfinder/PropertyControllerIT.java +++ b/src/test/java/org/phoebus/channelfinder/PropertyControllerIT.java @@ -19,10 +19,11 @@ import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Property; import org.phoebus.channelfinder.entity.Tag; +import org.phoebus.channelfinder.exceptions.PropertyNotFoundException; import org.phoebus.channelfinder.repository.ChannelRepository; import org.phoebus.channelfinder.repository.PropertyRepository; -import org.phoebus.channelfinder.rest.api.IProperty; -import org.phoebus.channelfinder.rest.controller.PropertyController; +import org.phoebus.channelfinder.web.legacy.api.IProperty; +import org.phoebus.channelfinder.web.legacy.controller.PropertyController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithMockUser; @@ -30,7 +31,6 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.server.ResponseStatusException; @TestInstance(TestInstance.Lifecycle.PER_CLASS) @WebMvcTest(PropertyController.class) // TODO Somehow creating one @@ -150,7 +150,7 @@ void readXmlProperty() { void readNonExistingXmlProperty() { // verify the property failed to be read, as expected Assertions.assertThrows( - ResponseStatusException.class, () -> propertyManager.read("fakeProperty", false)); + PropertyNotFoundException.class, () -> propertyManager.read("fakeProperty", false)); } /** attempt to read a single non existent property with channels */ @@ -158,7 +158,7 @@ void readNonExistingXmlProperty() { void readNonExistingXmlProperty2() { // verify the property failed to be read, as expected Assertions.assertThrows( - ResponseStatusException.class, () -> propertyManager.read("fakeProperty", true)); + PropertyNotFoundException.class, () -> propertyManager.read("fakeProperty", true)); } /** create a simple property */ diff --git a/src/test/java/org/phoebus/channelfinder/TagControllerIT.java b/src/test/java/org/phoebus/channelfinder/TagControllerIT.java index 951b1e3b..c52436b6 100644 --- a/src/test/java/org/phoebus/channelfinder/TagControllerIT.java +++ b/src/test/java/org/phoebus/channelfinder/TagControllerIT.java @@ -19,10 +19,11 @@ import org.phoebus.channelfinder.configuration.ElasticConfig; import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Tag; +import org.phoebus.channelfinder.exceptions.TagNotFoundException; import org.phoebus.channelfinder.repository.ChannelRepository; import org.phoebus.channelfinder.repository.TagRepository; -import org.phoebus.channelfinder.rest.api.ITag; -import org.phoebus.channelfinder.rest.controller.TagController; +import org.phoebus.channelfinder.web.legacy.api.ITag; +import org.phoebus.channelfinder.web.legacy.controller.TagController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithMockUser; @@ -30,7 +31,6 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.server.ResponseStatusException; @TestInstance(TestInstance.Lifecycle.PER_CLASS) @WebMvcTest(TagController.class) @@ -102,14 +102,14 @@ void readXmlTag() { @Test void readNonExistingXmlTag() { // verify the tag failed to be read, as expected - Assertions.assertThrows(ResponseStatusException.class, () -> tagManager.read("fakeTag", false)); + Assertions.assertThrows(TagNotFoundException.class, () -> tagManager.read("fakeTag", false)); } /** attempt to read a single non existent tag with channels */ @Test void readNonExistingXmlTag2() { // verify the tag failed to be read, as expected - Assertions.assertThrows(ResponseStatusException.class, () -> tagManager.read("fakeTag", true)); + Assertions.assertThrows(TagNotFoundException.class, () -> tagManager.read("fakeTag", true)); } /** create a simple tag */ diff --git a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderChannelsIT.java b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderChannelsIT.java index b0a60e1b..b82700d7 100644 --- a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderChannelsIT.java +++ b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderChannelsIT.java @@ -29,7 +29,7 @@ import org.phoebus.channelfinder.common.CFResourceDescriptors; import org.phoebus.channelfinder.docker.ITUtil.AuthorizationChoice; import org.phoebus.channelfinder.entity.Channel; -import org.phoebus.channelfinder.rest.controller.ChannelController; +import org.phoebus.channelfinder.web.legacy.controller.ChannelController; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; diff --git a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderPropertiesIT.java b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderPropertiesIT.java index 9f1c934a..be2a685f 100644 --- a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderPropertiesIT.java +++ b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderPropertiesIT.java @@ -30,7 +30,7 @@ import org.phoebus.channelfinder.docker.ITUtil.AuthorizationChoice; import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Property; -import org.phoebus.channelfinder.rest.controller.PropertyController; +import org.phoebus.channelfinder.web.legacy.controller.PropertyController; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; diff --git a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderScrollIT.java b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderScrollIT.java index 32e96874..f172a5a0 100644 --- a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderScrollIT.java +++ b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderScrollIT.java @@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test; import org.phoebus.channelfinder.common.CFResourceDescriptors; import org.phoebus.channelfinder.entity.Scroll; -import org.phoebus.channelfinder.rest.controller.ChannelScrollController; +import org.phoebus.channelfinder.web.legacy.controller.ChannelScrollController; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; diff --git a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderTagsIT.java b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderTagsIT.java index abe968c9..87b77e24 100644 --- a/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderTagsIT.java +++ b/src/test/java/org/phoebus/channelfinder/docker/ChannelFinderTagsIT.java @@ -30,7 +30,7 @@ import org.phoebus.channelfinder.docker.ITUtil.AuthorizationChoice; import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.rest.controller.TagController; +import org.phoebus.channelfinder.web.legacy.controller.TagController; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; diff --git a/src/test/java/org/phoebus/channelfinder/epics/EpicsRPCRequestIT.java b/src/test/java/org/phoebus/channelfinder/epics/EpicsRPCRequestIT.java index 3ee3bc8a..5ec172a3 100644 --- a/src/test/java/org/phoebus/channelfinder/epics/EpicsRPCRequestIT.java +++ b/src/test/java/org/phoebus/channelfinder/epics/EpicsRPCRequestIT.java @@ -19,10 +19,10 @@ import org.phoebus.channelfinder.entity.Channel; import org.phoebus.channelfinder.entity.Property; import org.phoebus.channelfinder.entity.Tag; -import org.phoebus.channelfinder.rest.api.IChannel; -import org.phoebus.channelfinder.rest.api.IProperty; -import org.phoebus.channelfinder.rest.api.ITag; import org.phoebus.channelfinder.service.ChannelFinderEpicsService; +import org.phoebus.channelfinder.web.legacy.api.IChannel; +import org.phoebus.channelfinder.web.legacy.api.IProperty; +import org.phoebus.channelfinder.web.legacy.api.ITag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithMockUser; diff --git a/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorControllerIT.java b/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorControllerIT.java index 5787daa4..913d52af 100644 --- a/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorControllerIT.java +++ b/src/test/java/org/phoebus/channelfinder/processors/ChannelProcessorControllerIT.java @@ -12,12 +12,11 @@ import org.mockito.Mockito; import org.phoebus.channelfinder.common.CFResourceDescriptors; import org.phoebus.channelfinder.entity.Scroll; -import org.phoebus.channelfinder.rest.api.IChannelScroll; -import org.phoebus.channelfinder.rest.controller.ChannelProcessorController; +import org.phoebus.channelfinder.service.ChannelScrollService; import org.phoebus.channelfinder.service.external.ArchiverService; +import org.phoebus.channelfinder.web.legacy.controller.ChannelProcessorController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -36,7 +35,7 @@ class ChannelProcessorControllerIT { "Basic " + Base64.getEncoder().encodeToString("admin:adminPass".getBytes()); @Autowired protected MockMvc mockMvc; - @MockBean IChannelScroll channelScroll; + @MockitoBean ChannelScrollService channelScrollService; @MockitoBean ArchiverService archiverService; @Test @@ -71,7 +70,8 @@ void testProcessorEnabled() throws Exception { @Test void testProcessAllChannels() throws Exception { - Mockito.when(channelScroll.query(Mockito.any())).thenReturn(new Scroll("", List.of())); + Mockito.when(channelScrollService.search(Mockito.any(), Mockito.any())) + .thenReturn(new Scroll("", List.of())); MockHttpServletRequestBuilder request = put("/" + CFResourceDescriptors.CHANNEL_PROCESSOR_RESOURCE_URI + "/process/all") @@ -81,7 +81,8 @@ void testProcessAllChannels() throws Exception { @Test void testProcessQuery() throws Exception { - Mockito.when(channelScroll.query(Mockito.any())).thenReturn(new Scroll("", List.of())); + Mockito.when(channelScrollService.search(Mockito.any(), Mockito.any())) + .thenReturn(new Scroll("", List.of())); MockHttpServletRequestBuilder request = put("/" + CFResourceDescriptors.CHANNEL_PROCESSOR_RESOURCE_URI + "/process/query") .header(HttpHeaders.AUTHORIZATION, AUTHORIZATION); From 2c90bcd4287063435535213b11af841ee4928552 Mon Sep 17 00:00:00 2001 From: Anders Lindh Olsson Date: Thu, 2 Apr 2026 13:47:40 +0200 Subject: [PATCH 5/6] fix(security): centralise CORS policy in WebSecurityConfig Replace the implicit per-controller @CrossOrigin annotations with an explicit CorsConfigurationSource bean in WebSecurityConfig, backed by a configurable cors.allowed-origins property (default: *). - Remove @CrossOrigin from all legacy controllers (committed as fixup). - Add corsConfigurationSource() bean and wire it into the filter chain. - Document the property in application.properties. --- .../configuration/WebSecurityConfig.java | 18 ++++++++++++++++++ src/main/resources/application.properties | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/org/phoebus/channelfinder/configuration/WebSecurityConfig.java b/src/main/java/org/phoebus/channelfinder/configuration/WebSecurityConfig.java index e25afb50..8ddc25d9 100644 --- a/src/main/java/org/phoebus/channelfinder/configuration/WebSecurityConfig.java +++ b/src/main/java/org/phoebus/channelfinder/configuration/WebSecurityConfig.java @@ -27,15 +27,22 @@ import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration public class WebSecurityConfig { + @Value("${cors.allowed-origins:*}") + private List corsAllowedOrigins; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // CSRF disabled: application is a stateless REST API using HTTP Basic auth. // No session or cookie-based authentication is used, so CSRF attacks are not applicable. return http.csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) @@ -43,6 +50,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .build(); } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOriginPatterns(corsAllowedOrigins); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + @Bean public WebSecurityCustomizer ignoringCustomizer() { // Authentication and Authorization is only needed for non search/query operations diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b78d2ba0..e30952fd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -111,6 +111,10 @@ elasticsearch.create.indices=true # Repository chunk size, how many channels to submit to elastic at once repository.chunk.size = 10000 +############################## CORS ############################### +# Comma-separated list of allowed origins (supports wildcards). Default: allow all. +cors.allowed-origins=* + ############################## Legacy API path ############################### # First path segment shared by all legacy API endpoints (e.g. ChannelFinder/resources/channels). # Change this for multi-service deployments to distinguish services at a shared reverse proxy. From fb0a3380e97a592825d4903e38945df9db8957c1 Mon Sep 17 00:00:00 2001 From: Anders Lindh Olsson Date: Thu, 2 Apr 2026 14:19:14 +0200 Subject: [PATCH 6/6] fix(test): chunk PopulateDBConfiguration bulk inserts and fail fast on errors Bulk requests of 1500+ channels with rich property payloads exceeded the default ES circuit-breaker limit (~102 MiB), causing 429/413 rejections in CI. Split channel inserts into batches of 1000 and throw on bulk errors rather than logging and continuing with a silently empty index. Same fail-fast treatment applied to checkBulkResponse (properties/tags). --- .../PopulateDBConfiguration.java | 105 ++++++++++-------- .../ChannelRepositorySearchIT.java | 2 +- .../ChannelScrollControllerSearchIT.java | 2 +- .../performance/ExistsPerformanceIT.java | 3 +- .../PopulateDBConfigurationIT.java | 3 +- 5 files changed, 62 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/phoebus/channelfinder/configuration/PopulateDBConfiguration.java b/src/main/java/org/phoebus/channelfinder/configuration/PopulateDBConfiguration.java index 59c42d89..17dc96fa 100644 --- a/src/main/java/org/phoebus/channelfinder/configuration/PopulateDBConfiguration.java +++ b/src/main/java/org/phoebus/channelfinder/configuration/PopulateDBConfiguration.java @@ -179,7 +179,7 @@ public synchronized void cleanupDB() { } } - public synchronized void createDB(int cells) { + public synchronized void createDB(int cells) throws IOException { numberOfCells = cells; createDB(); } @@ -188,7 +188,7 @@ public Set getChannelList() { return channelList; } - public synchronized void createDB() { + public synchronized void createDB() throws IOException { int freq = 25; Collection channels = new ArrayList<>(); createSRChannels(channels, freq); @@ -221,35 +221,31 @@ public synchronized void createDB() { logger.log(Level.INFO, "completed populating"); } - private void checkBulkResponse(BulkRequest.Builder br) { + private void checkBulkResponse(BulkRequest.Builder br) throws IOException { + BulkResponse results; try { - BulkResponse results = client.bulk(br.build()); - if (results.errors()) { - logger.log(Level.SEVERE, "CreateDB Bulk had errors"); - for (BulkResponseItem item : results.items()) { - if (item.error() != null) { - logger.log(Level.SEVERE, () -> item.error().reason()); - } + results = client.bulk(br.build()); + } catch (IOException e) { + throw new IOException("CreateDB bulk operation failed", e); + } + if (results.errors()) { + StringBuilder errors = new StringBuilder("CreateDB bulk had errors:"); + for (BulkResponseItem item : results.items()) { + if (item.error() != null) { + errors.append("\n ").append(item.error().reason()); } } - } catch (IOException e) { - logger.log(Level.WARNING, "CreateDB Bulk operation failed.", e); + throw new IOException(errors.toString()); } } - private void bulkInsertAllChannels(Collection channels) { - try { - logger.info("Bulk inserting channels"); - - bulkInsertChannels(channels); - channels.clear(); - - } catch (Exception e) { - logger.log(Level.WARNING, e.getMessage(), e); - } + private void bulkInsertAllChannels(Collection channels) throws IOException { + logger.info("Bulk inserting channels"); + bulkInsertChannels(channels); + channels.clear(); } - private void createBOChannels(Collection channels, int freq) { + private void createBOChannels(Collection channels, int freq) throws IOException { logger.info(() -> "Creating BO channels"); for (int i = 1; i <= numberOfCells; i++) { @@ -263,7 +259,7 @@ private void createBOChannels(Collection channels, int freq) { } } - private void createSRChannels(Collection channels, int freq) { + private void createSRChannels(Collection channels, int freq) throws IOException { logger.info("Creating SR channels"); for (int i = 1; i <= numberOfCells; i++) { @@ -400,32 +396,43 @@ private Collection insertSRCell(String cell) { return result; } - private void bulkInsertChannels(Collection result) throws IOException { - long start = System.currentTimeMillis(); - BulkRequest.Builder br = new BulkRequest.Builder(); - for (Channel channel : result) { - br.operations( - op -> - op.index( - IndexOperation.of( - i -> - i.index(esService.getES_CHANNEL_INDEX()) - .id(channel.getName()) - .document(channel)))); - } - String prepare = "|Prepare: " + (System.currentTimeMillis() - start) + "|"; - start = System.currentTimeMillis(); - br.refresh(Refresh.True); - - BulkResponse srResult = client.bulk(br.build()); - String execute = "|Execute: " + (System.currentTimeMillis() - start) + "|"; - logger.log(Level.INFO, () -> "Inserted cell " + prepare + " " + execute); - if (srResult.errors()) { - logger.log(Level.SEVERE, "Bulk insert had errors"); - for (BulkResponseItem item : srResult.items()) { - if (item.error() != null) { - logger.log(Level.SEVERE, () -> item.error().reason()); + private static final int BULK_INSERT_BATCH_SIZE = 1000; + + private void bulkInsertChannels(Collection channels) throws IOException { + List list = new ArrayList<>(channels); + for (int offset = 0; offset < list.size(); offset += BULK_INSERT_BATCH_SIZE) { + List batch = + list.subList(offset, Math.min(offset + BULK_INSERT_BATCH_SIZE, list.size())); + int batchCount = batch.size(); + long t0 = System.currentTimeMillis(); + BulkRequest.Builder br = new BulkRequest.Builder(); + for (Channel channel : batch) { + br.operations( + op -> + op.index( + IndexOperation.of( + i -> + i.index(esService.getES_CHANNEL_INDEX()) + .id(channel.getName()) + .document(channel)))); + } + String prepare = "|Prepare: " + (System.currentTimeMillis() - t0) + "|"; + t0 = System.currentTimeMillis(); + br.refresh(Refresh.True); + BulkResponse result = client.bulk(br.build()); + String execute = "|Execute: " + (System.currentTimeMillis() - t0) + "|"; + logger.log( + Level.INFO, + () -> "Inserted batch (" + batchCount + " channels) " + prepare + " " + execute); + if (result.errors()) { + StringBuilder errors = new StringBuilder("Bulk insert batch had errors:"); + for (BulkResponseItem item : result.items()) { + if (item.error() != null) { + errors.append("\n ").append(item.error().reason()); + } } + logger.log(Level.SEVERE, errors::toString); + throw new IOException(errors.toString()); } } } diff --git a/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java b/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java index 7b887e6e..bb4c367a 100644 --- a/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java +++ b/src/test/java/org/phoebus/channelfinder/ChannelRepositorySearchIT.java @@ -60,7 +60,7 @@ void setupAll() { } @BeforeEach - public void setup() throws InterruptedException { + void setup() throws InterruptedException, IOException { populateDBConfiguration.cleanupDB(); populateDBConfiguration.createDB(CELLS); Thread.sleep(5000); diff --git a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java index 0d5a4e29..70541d6f 100644 --- a/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java +++ b/src/test/java/org/phoebus/channelfinder/ChannelScrollControllerSearchIT.java @@ -41,7 +41,7 @@ class ChannelScrollControllerSearchIT { @Autowired PopulateDBConfiguration populateDBConfiguration; @BeforeEach - public void setup() throws InterruptedException { + public void setup() throws InterruptedException, IOException { populateDBConfiguration.createDB(1); Thread.sleep(10000); } diff --git a/src/test/java/org/phoebus/channelfinder/performance/ExistsPerformanceIT.java b/src/test/java/org/phoebus/channelfinder/performance/ExistsPerformanceIT.java index a46667cb..4446418d 100644 --- a/src/test/java/org/phoebus/channelfinder/performance/ExistsPerformanceIT.java +++ b/src/test/java/org/phoebus/channelfinder/performance/ExistsPerformanceIT.java @@ -1,6 +1,7 @@ package org.phoebus.channelfinder.performance; import com.google.common.collect.Lists; +import java.io.IOException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.phoebus.channelfinder.configuration.PopulateDBConfiguration; @@ -21,7 +22,7 @@ class ExistsPerformanceIT { @Autowired ChannelRepository channelRepository; @Test - void channelExists() { + void channelExists() throws IOException { service.createDB(1); Assertions.assertTrue( channelRepository.existsByIds(Lists.newArrayList(service.getChannelList()))); diff --git a/src/test/java/org/phoebus/channelfinder/performance/PopulateDBConfigurationIT.java b/src/test/java/org/phoebus/channelfinder/performance/PopulateDBConfigurationIT.java index fa2290a2..06a169cd 100644 --- a/src/test/java/org/phoebus/channelfinder/performance/PopulateDBConfigurationIT.java +++ b/src/test/java/org/phoebus/channelfinder/performance/PopulateDBConfigurationIT.java @@ -1,6 +1,7 @@ package org.phoebus.channelfinder.performance; import java.net.URL; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.phoebus.channelfinder.configuration.PopulateDBConfiguration; import org.phoebus.channelfinder.service.AuthorizationService; @@ -27,7 +28,7 @@ void testCreateTagsAndProperties() { @Test void testCreateDB() { - populateDBConfiguration.createDB(); + Assertions.assertDoesNotThrow(() -> populateDBConfiguration.createDB()); } @Test