diff --git a/lexer/src/main/antlr4/gov/nasa/pds/api/registry/lexer/Search.g4 b/lexer/src/main/antlr4/gov/nasa/pds/api/registry/lexer/Search.g4 index 68e1ebd3..dbccf1f7 100644 --- a/lexer/src/main/antlr4/gov/nasa/pds/api/registry/lexer/Search.g4 +++ b/lexer/src/main/antlr4/gov/nasa/pds/api/registry/lexer/Search.g4 @@ -10,25 +10,54 @@ comparison : FIELD operator ( NUMBER | STRINGVAL ) ; likeComparison : FIELD LIKE STRINGVAL ; operator : EQ | NE | GT | GE | LT | LE ; -NOT : 'not' ; +NOT : 'NOT' | 'not' ; -EQ : 'eq' ; -NE : 'ne' ; -GT : 'gt' ; -GE : 'ge' ; -LT : 'lt' ; -LE : 'le' ; +EQ : E Q ; +NE : N E ; +GT : G T ; +GE : G E ; +LT : L T ; +LE : L E ; -LIKE: 'like'; +LIKE: L I K E; LPAREN : '(' ; RPAREN : ')' ; -AND : 'AND' | 'and' ; -OR : 'OR' | 'or' ; +AND : A N D ; +OR : O R ; FIELD : [A-Za-z_] [A-Za-z0-9_.:/]* ; STRINGVAL : '"' ~["\r\n]* '"' ; NUMBER : ('-')? [0-9]+ ('.' [0-9]*)? ; WS : [ \t\r\n]+ -> skip ; + + +// case-insensitivity fragments, per https://chromium.googlesource.com/external/github.com/antlr/antlr4/+/2191c386190a7d57d457319dd2f6aec4f0231d4c/doc/case-insensitive-lexing.md +fragment A : [aA]; +fragment B : [bB]; +fragment C : [cC]; +fragment D : [dD]; +fragment E : [eE]; +fragment F : [fF]; +fragment G : [gG]; +fragment H : [hH]; +fragment I : [iI]; +fragment J : [jJ]; +fragment K : [kK]; +fragment L : [lL]; +fragment M : [mM]; +fragment N : [nN]; +fragment O : [oO]; +fragment P : [pP]; +fragment Q : [qQ]; +fragment R : [rR]; +fragment S : [sS]; +fragment T : [tT]; +fragment U : [uU]; +fragment V : [vV]; +fragment W : [wW]; +fragment X : [xX]; +fragment Y : [yY]; +fragment Z : [zZ]; \ No newline at end of file diff --git a/model/swagger.yml b/model/swagger.yml index 9f0839c8..f1a7a31f 100644 --- a/model/swagger.yml +++ b/model/swagger.yml @@ -119,7 +119,7 @@ paths: tags: - 5. all docs summary: | - search on all registry documents by posting an OpenSearch DSL query + WORK IN PROGRESS: search on all registry documents by posting an OpenSearch DSL query operationId: docs requestBody: description: OpenSearch DSL query @@ -278,6 +278,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" /products/{identifier}/members/{versions}: @@ -302,6 +303,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" - $ref: "#/components/parameters/Versions" @@ -327,6 +329,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" /products/{identifier}/members/members/{versions}: @@ -351,6 +354,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" - $ref: "#/components/parameters/Versions" @@ -376,6 +380,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" /products/{identifier}/member-of/{versions}: @@ -400,6 +405,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" - $ref: "#/components/parameters/Versions" @@ -425,6 +431,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" /products/{identifier}/member-of/member-of/{versions}: @@ -449,6 +456,7 @@ paths: - $ref: "#/components/parameters/Fields" - $ref: "#/components/parameters/Identifier" - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/Query" - $ref: "#/components/parameters/Sort" - $ref: "#/components/parameters/SearchAfter" - $ref: "#/components/parameters/Versions" @@ -660,7 +668,7 @@ components: content: "*": schema: - type: object + $ref: '#/components/schemas/propertiesList' "*/*": schema: $ref: '#/components/schemas/propertiesList' diff --git a/service/src/main/java/gov/nasa/pds/api/registry/configuration/WebMVCConfig.java b/service/src/main/java/gov/nasa/pds/api/registry/configuration/WebMVCConfig.java index fc9a55a7..5a7202fe 100644 --- a/service/src/main/java/gov/nasa/pds/api/registry/configuration/WebMVCConfig.java +++ b/service/src/main/java/gov/nasa/pds/api/registry/configuration/WebMVCConfig.java @@ -1,6 +1,9 @@ package gov.nasa.pds.api.registry.configuration; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -15,6 +18,11 @@ import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import gov.nasa.pds.api.registry.controllers.ProductsController; +import gov.nasa.pds.api.registry.model.api_responses.PdsProductBusinessObject; +import gov.nasa.pds.api.registry.model.api_responses.ProductBusinessLogic; +import gov.nasa.pds.api.registry.model.api_responses.WyriwygBusinessObject; +import gov.nasa.pds.api.registry.model.exceptions.AcceptFormatNotSupportedException; import gov.nasa.pds.api.registry.view.CsvErrorMessageSerializer; import gov.nasa.pds.api.registry.view.CsvPluralSerializer; import gov.nasa.pds.api.registry.view.CsvSingularSerializer; @@ -39,9 +47,37 @@ public class WebMVCConfig implements WebMvcConfigurer { private static final Logger log = LoggerFactory.getLogger(WebMVCConfig.class); + + @Value("${server.contextPath}") private String contextPath; + private static Map> formatters = + new HashMap>(); + + static public Map> getFormatters() { + return formatters; + } + + static { + // TODO move that at a better place, it is not specific to this controller + formatters.put("*", PdsProductBusinessObject.class); + formatters.put("*/*", PdsProductBusinessObject.class); + formatters.put("application/csv", WyriwygBusinessObject.class); + formatters.put("application/json", PdsProductBusinessObject.class); + formatters.put("application/kvp+json", WyriwygBusinessObject.class); + // this.formatters.put("application/vnd.nasa.pds.pds4+json", new + // Pds4ProductBusinessObject(true)); + // this.formatters.put("application/vnd.nasa.pds.pds4+xml", new + // Pds4ProductBusinessObject(false)); + formatters.put("application/xml", PdsProductBusinessObject.class); + formatters.put("text/csv", WyriwygBusinessObject.class); + formatters.put("text/html", PdsProductBusinessObject.class); + formatters.put("text/xml", PdsProductBusinessObject.class); + } + + + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String contextPath = this.contextPath.endsWith("/") ? this.contextPath : this.contextPath + "/"; @@ -120,4 +156,27 @@ public void configureMessageConverters(List> converters) WebMVCConfig.log.info("Number of converters available after adding locals " + Integer.toString(converters.size())); } + + + + static public Class selectFormatterClass(String acceptHeaderValue) + throws AcceptFormatNotSupportedException { + + + // split by , and remove extra spaces + String[] acceptOrderedValues = + Arrays.stream(acceptHeaderValue.split(",")).map(String::trim).toArray(String[]::new); + + for (String acceptValue : acceptOrderedValues) { + if (WebMVCConfig.formatters.containsKey(acceptValue)) { + return WebMVCConfig.formatters.get(acceptValue); + } + } + + // if none of the Accept format proposed matches + throw new AcceptFormatNotSupportedException( + "None of the format(s) " + acceptHeaderValue + " is supported."); + + } + } diff --git a/service/src/main/java/gov/nasa/pds/api/registry/controllers/ProductsController.java b/service/src/main/java/gov/nasa/pds/api/registry/controllers/ProductsController.java index 2dba1106..1a040f7f 100644 --- a/service/src/main/java/gov/nasa/pds/api/registry/controllers/ProductsController.java +++ b/service/src/main/java/gov/nasa/pds/api/registry/controllers/ProductsController.java @@ -3,18 +3,25 @@ import java.lang.reflect.InvocationTargetException; import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; import gov.nasa.pds.api.base.ClassesApi; +import gov.nasa.pds.api.base.PropertiesApi; import gov.nasa.pds.api.registry.model.exceptions.*; import gov.nasa.pds.api.registry.model.identifiers.PdsLid; import gov.nasa.pds.api.registry.model.identifiers.PdsLidVid; import gov.nasa.pds.api.registry.model.identifiers.PdsProductClasses; +import gov.nasa.pds.api.registry.model.properties.PdsProperty; import jakarta.servlet.http.HttpServletRequest; import com.fasterxml.jackson.databind.ObjectMapper; import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch._types.OpenSearchException; +import org.opensearch.client.opensearch._types.mapping.Property; import org.opensearch.client.opensearch.core.SearchRequest; import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.indices.GetMappingRequest; +import org.opensearch.client.opensearch.indices.GetMappingResponse; +import org.opensearch.client.opensearch.indices.OpenSearchIndicesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +34,7 @@ import org.springframework.web.context.request.ServletRequestAttributes; import gov.nasa.pds.api.base.ProductsApi; import gov.nasa.pds.api.registry.ConnectionContext; +import gov.nasa.pds.api.registry.configuration.WebMVCConfig; import gov.nasa.pds.api.registry.model.ErrorMessageFactory; import gov.nasa.pds.api.registry.model.api_responses.PdsProductBusinessObject; import gov.nasa.pds.api.registry.model.api_responses.ProductBusinessLogic; @@ -35,16 +43,18 @@ import gov.nasa.pds.api.registry.model.api_responses.WyriwygBusinessObject; import gov.nasa.pds.api.registry.model.identifiers.PdsProductIdentifier; import gov.nasa.pds.api.registry.search.RegistrySearchRequestBuilder; +import gov.nasa.pds.model.PropertiesListInner; @Controller // TODO: Refactor common controller code out of ProductsController and split the additional API implementations out into // corresponding controllers -public class ProductsController implements ProductsApi, ClassesApi { +public class ProductsController implements ProductsApi, ClassesApi, PropertiesApi { @Override - // TODO: Remove this when the common controller code is refactored out - it is only necessary because additional + // TODO: Remove this when the common controller code is refactored out - it is only necessary + // because additional // interfaces have been implemented as a stopgap public Optional getRequest() { return ProductsApi.super.getRequest(); @@ -58,34 +68,10 @@ public Optional getRequest() { private OpenSearchClient openSearchClient; private SearchRequest presetSearchRequest; - // TODO move that at a better place, it is not specific to this controller - private static Map> formatters = - new HashMap>(); - - static Map> getFormatters() { - return formatters; - } - static Integer DEFAULT_LIMIT = 100; - static { - // TODO move that at a better place, it is not specific to this controller - formatters.put("*", PdsProductBusinessObject.class); - formatters.put("*/*", PdsProductBusinessObject.class); - formatters.put("application/csv", WyriwygBusinessObject.class); - formatters.put("application/json", PdsProductBusinessObject.class); - formatters.put("application/kvp+json", WyriwygBusinessObject.class); - // this.formatters.put("application/vnd.nasa.pds.pds4+json", new - // Pds4ProductBusinessObject(true)); - // this.formatters.put("application/vnd.nasa.pds.pds4+xml", new - // Pds4ProductBusinessObject(false)); - formatters.put("application/xml", PdsProductBusinessObject.class); - formatters.put("text/csv", WyriwygBusinessObject.class); - formatters.put("text/html", PdsProductBusinessObject.class); - formatters.put("text/xml", PdsProductBusinessObject.class); - } - + // TODO move that at a better place, it is not specific to this controller @Autowired public ProductsController(ConnectionContext connectionContext, ErrorMessageFactory errorMessageFactory, ObjectMapper objectMapper) { @@ -97,6 +83,7 @@ public ProductsController(ConnectionContext connectionContext, } + private ResponseEntity formatSingleProduct(HashMap product, List fields) throws AcceptFormatNotSupportedException, UnhandledException { // TODO add case when Accept is not available, default application/json @@ -104,13 +91,8 @@ private ResponseEntity formatSingleProduct(HashMap produ ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); String acceptHeaderValue = curRequest.getHeader("Accept"); - if (!ProductsController.formatters.containsKey(acceptHeaderValue)) { - throw new AcceptFormatNotSupportedException( - "format " + acceptHeaderValue + "is not supported."); - } - Class formatterClass = - ProductsController.formatters.get(acceptHeaderValue); + WebMVCConfig.selectFormatterClass(acceptHeaderValue); try { // TODO replace URLs from the request path @@ -137,13 +119,9 @@ private ResponseEntity formatMultipleProducts(RawMultipleProductResponse ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); String acceptHeaderValue = curRequest.getHeader("Accept"); - if (!ProductsController.formatters.containsKey(acceptHeaderValue)) { - throw new AcceptFormatNotSupportedException( - "format " + acceptHeaderValue + " is not supported."); - } - Class formatterClass = - ProductsController.formatters.get(acceptHeaderValue); + WebMVCConfig.selectFormatterClass(acceptHeaderValue); + try { // TODO replace URLs from the request path @@ -249,12 +227,7 @@ public ResponseEntity productList(List fields, List keyw Integer limit, String q, List sort, List searchAfter) throws Exception { SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .constrainByQueryString(q) - .addKeywordsParam(keywords) - .fieldsFromStrings(fields) - .paginate(limit, sort, searchAfter) - .onlyLatest() - .build(); + .applyMultipleProductsDefaults(fields, q, keywords, limit, sort, searchAfter, true).build(); SearchResponse searchResponse = this.openSearchClient.search(searchRequest, HashMap.class); @@ -273,9 +246,7 @@ private HashMap getLidVid(PdsProductIdentifier identifier, List< throws OpenSearchException, IOException, NotFoundException { SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchLidvid(identifier) - .fieldsFromStrings(fields) - .build(); + .matchLidvid(identifier).fieldsFromStrings(fields).build(); // useless to detail here that the HashMap is parameterized // because of compilation features, see @@ -297,10 +268,7 @@ private HashMap getLatestLidVid(PdsProductIdentifier identifier, List fields) throws OpenSearchException, IOException, NotFoundException { SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchLid(identifier) - .fieldsFromStrings(fields) - .onlyLatest() - .build(); + .matchLid(identifier).fieldsFromStrings(fields).excludeSupersededProducts().build(); // useless to detail here that the HashMap is parameterized // because of compilation features, see @@ -324,9 +292,7 @@ private RawMultipleProductResponse getAllLidVid(PdsProductIdentifier identifier, throws OpenSearchException, IOException, NotFoundException, SortSearchAfterMismatchException { SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchLid(identifier).fieldsFromStrings(fields) - .paginate(limit, sort, searchAfter) - .build(); + .matchLid(identifier).fieldsFromStrings(fields).paginate(limit, sort, searchAfter).build(); // useless to detail here that the HashMap is parameterized // because of compilation features, see @@ -343,34 +309,33 @@ private RawMultipleProductResponse getAllLidVid(PdsProductIdentifier identifier, } private PdsProductClasses resolveProductClass(PdsProductIdentifier identifier) - throws OpenSearchException, IOException, NotFoundException{ + throws OpenSearchException, IOException, NotFoundException { SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchLid(identifier) - .fieldsFromStrings(List.of(PdsProductClasses.getPropertyName())) - .onlyLatest() - .build(); + .matchLid(identifier).fieldsFromStrings(List.of(PdsProductClasses.getPropertyName())) + .excludeSupersededProducts().build(); - SearchResponse searchResponse = this.openSearchClient.search(searchRequest, HashMap.class); + SearchResponse searchResponse = + this.openSearchClient.search(searchRequest, HashMap.class); if (searchResponse.hits().total().value() == 0) { throw new NotFoundException("No product found with identifier " + identifier.toString()); } - String productClassStr = searchResponse.hits().hits().get(0).source().get(PdsProductClasses.getPropertyName()).toString(); + String productClassStr = searchResponse.hits().hits().get(0).source() + .get(PdsProductClasses.getPropertyName()).toString(); return PdsProductClasses.valueOf(productClassStr); } private PdsLidVid resolveLatestLidvid(PdsProductIdentifier identifier) - throws OpenSearchException, IOException, NotFoundException { + throws OpenSearchException, IOException, NotFoundException { - SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchLid(identifier.getLid()) - .fieldsFromStrings(List.of()) - .onlyLatest() - .build(); + SearchRequest searchRequest = + new RegistrySearchRequestBuilder(this.connectionContext).matchLid(identifier.getLid()) + .fieldsFromStrings(List.of()).excludeSupersededProducts().build(); - SearchResponse searchResponse = this.openSearchClient.search(searchRequest, HashMap.class); + SearchResponse searchResponse = + this.openSearchClient.search(searchRequest, HashMap.class); if (searchResponse.hits().total().value() == 0) { throw new NotFoundException("No lidvids found with lid " + identifier.getLid().toString()); @@ -383,46 +348,49 @@ private PdsLidVid resolveLatestLidvid(PdsProductIdentifier identifier) private List resolveExtantLidvids(PdsLid lid) - throws OpenSearchException, IOException, NotFoundException{ + throws OpenSearchException, IOException, NotFoundException { String lidvidKey = "_id"; SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchLid(lid) - .fieldsFromStrings(List.of(lidvidKey)) - .build(); + .matchLid(lid).fieldsFromStrings(List.of(lidvidKey)).build(); - SearchResponse searchResponse = this.openSearchClient.search(searchRequest, HashMap.class); + SearchResponse searchResponse = + this.openSearchClient.search(searchRequest, HashMap.class); if (searchResponse.hits().total().value() == 0) { throw new NotFoundException("No lidvids found with lid " + lid.toString()); } - return searchResponse.hits().hits().stream().map(hit -> hit.source().get(lidvidKey).toString()).map(PdsLidVid::fromString).toList(); + return searchResponse.hits().hits().stream().map(hit -> hit.source().get(lidvidKey).toString()) + .map(PdsLidVid::fromString).toList(); } /** - * Resolve a PdsProductIdentifier to a PdsLidVid according to the common rules of the API. - * The rules are currently trivial, but may incorporate additional behaviour later + * Resolve a PdsProductIdentifier to a PdsLidVid according to the common rules of the API. The + * rules are currently trivial, but may incorporate additional behaviour later + * * @param identifier a LID or LIDVID * @return a LIDVID */ - private PdsLidVid resolveIdentifierToLidvid(PdsProductIdentifier identifier) throws NotFoundException, IOException { + private PdsLidVid resolveIdentifierToLidvid(PdsProductIdentifier identifier) + throws NotFoundException, IOException { return identifier.isLidvid() ? (PdsLidVid) identifier : resolveLatestLidvid(identifier); } @Override - public ResponseEntity productMembers( - String identifier, List fields, Integer limit, List sort, List searchAfter) - throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, BadRequestException, - AcceptFormatNotSupportedException{ + public ResponseEntity productMembers(String identifier, List fields, + Integer limit, String q, List sort, List searchAfter) + throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, + BadRequestException, AcceptFormatNotSupportedException { - try{ + try { PdsProductIdentifier pdsIdentifier = PdsProductIdentifier.fromString(identifier); PdsProductClasses productClass = resolveProductClass(pdsIdentifier); PdsLidVid lidvid = resolveIdentifierToLidvid(pdsIdentifier); - RegistrySearchRequestBuilder searchRequestBuilder = new RegistrySearchRequestBuilder(this.connectionContext); + RegistrySearchRequestBuilder searchRequestBuilder = + new RegistrySearchRequestBuilder(this.connectionContext); if (productClass.isBundle()) { searchRequestBuilder.matchMembersOfBundle(lidvid); @@ -431,134 +399,139 @@ public ResponseEntity productMembers( searchRequestBuilder.matchMembersOfCollection(lidvid); searchRequestBuilder.onlyBasicProducts(); } else { - throw new BadRequestException("productMembers endpoint is only valid for products with Product_Class '" + - PdsProductClasses.Product_Bundle + "' or '" + PdsProductClasses.Product_Collection + - "' (got '" + productClass + "')"); + throw new BadRequestException( + "productMembers endpoint is only valid for products with Product_Class '" + + PdsProductClasses.Product_Bundle + "' or '" + PdsProductClasses.Product_Collection + + "' (got '" + productClass + "')"); } SearchRequest searchRequest = searchRequestBuilder - .fieldsFromStrings(fields) - .paginate(limit, sort, searchAfter) - .onlyLatest() - .build(); + .applyMultipleProductsDefaults(fields, q, List.of(), limit, sort, searchAfter, true) + .build(); SearchResponse searchResponse = - this.openSearchClient.search(searchRequest, HashMap.class); + this.openSearchClient.search(searchRequest, HashMap.class); RawMultipleProductResponse products = new RawMultipleProductResponse(searchResponse); return formatMultipleProducts(products, fields); - } catch (IOException | OpenSearchException e) { + } catch (IOException | OpenSearchException | UnparsableQParamException e) { throw new UnhandledException(e); } } @Override - public ResponseEntity productMembersMembers( - String identifier, List fields, Integer limit, List sort, List searchAfter) - throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, BadRequestException, - AcceptFormatNotSupportedException{ + public ResponseEntity productMembersMembers(String identifier, List fields, + Integer limit, String q, List sort, List searchAfter) + throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, + BadRequestException, AcceptFormatNotSupportedException { - try{ + try { PdsProductIdentifier pdsIdentifier = PdsProductIdentifier.fromString(identifier); PdsProductClasses productClass = resolveProductClass(pdsIdentifier); PdsLidVid lidvid = resolveIdentifierToLidvid(pdsIdentifier); - RegistrySearchRequestBuilder searchRequestBuilder = new RegistrySearchRequestBuilder(this.connectionContext); + RegistrySearchRequestBuilder searchRequestBuilder = + new RegistrySearchRequestBuilder(this.connectionContext); if (productClass.isBundle()) { searchRequestBuilder.matchMembersOfBundle(lidvid); searchRequestBuilder.onlyBasicProducts(); } else { - throw new BadRequestException("productMembers endpoint is only valid for products with Product_Class '" + - PdsProductClasses.Product_Bundle + "' (got '" + productClass + "')"); + throw new BadRequestException( + "productMembers endpoint is only valid for products with Product_Class '" + + PdsProductClasses.Product_Bundle + "' (got '" + productClass + "')"); } SearchRequest searchRequest = searchRequestBuilder - .fieldsFromStrings(fields) - .paginate(limit, sort, searchAfter) - .onlyLatest() - .build(); + .applyMultipleProductsDefaults(fields, q, List.of(), limit, sort, searchAfter, true) + .build(); SearchResponse searchResponse = - this.openSearchClient.search(searchRequest, HashMap.class); + this.openSearchClient.search(searchRequest, HashMap.class); RawMultipleProductResponse products = new RawMultipleProductResponse(searchResponse); return formatMultipleProducts(products, fields); - } catch (IOException | OpenSearchException e) { + } catch (IOException | OpenSearchException | UnparsableQParamException e) { throw new UnhandledException(e); } } /** - * Given a PdsProductIdentifier and the name of a document field which is expected to contain an array of LIDVID - * strings, return the chained contents of that field from all documents matching the identifier (multiple docs are - * possible if the identifier is a LID). + * Given a PdsProductIdentifier and the name of a document field which is expected to contain an + * array of LIDVID strings, return the chained contents of that field from all documents matching + * the identifier (multiple docs are possible if the identifier is a LID). + * * @param identifier the LID/LIDVID for which to retrieve documents * @param fieldName the name of the document _source property/field from which to extract results - * @return a deduplicated list of the aggregated property/field contents, converted to PdsProductLidvids + * @return a deduplicated list of the aggregated property/field contents, converted to + * PdsProductLidvids */ - private List resolveLidVidsFromProductField(PdsProductIdentifier identifier, String fieldName) - throws OpenSearchException, IOException, NotFoundException, UnhandledException { + private List resolveLidVidsFromProductField(PdsProductIdentifier identifier, + String fieldName) + throws OpenSearchException, IOException, NotFoundException, UnhandledException { - RegistrySearchRequestBuilder searchRequestBuilder = new RegistrySearchRequestBuilder(this.connectionContext); + RegistrySearchRequestBuilder searchRequestBuilder = + new RegistrySearchRequestBuilder(this.connectionContext); if (identifier.isLid()) { searchRequestBuilder.matchLid(identifier); } else if (identifier.isLidvid()) { searchRequestBuilder.matchLidvid(identifier); } else { - throw new UnhandledException("PdsProductIdentifier identifier is neither LID nor LIDVID. This should never occur"); + throw new UnhandledException( + "PdsProductIdentifier identifier is neither LID nor LIDVID. This should never occur"); } - SearchRequest searchRequest = searchRequestBuilder - .matchLid(identifier) - .fieldsFromStrings(List.of(fieldName)) - .build(); + SearchRequest searchRequest = + searchRequestBuilder.matchLid(identifier).fieldsFromStrings(List.of(fieldName)).build(); - SearchResponse searchResponse = this.openSearchClient.search(searchRequest, HashMap.class); + SearchResponse searchResponse = + this.openSearchClient.search(searchRequest, HashMap.class); if (searchResponse.hits().total().value() == 0) { throw new NotFoundException("No product found with identifier " + identifier); } - return searchResponse.hits().hits().stream().map(hit -> (List) hit.source().get(fieldName)).filter(Objects::nonNull).flatMap(Collection::stream).map(PdsLidVid::fromString).toList(); + return searchResponse.hits().hits().stream() + .map(hit -> (List) hit.source().get(fieldName)).filter(Objects::nonNull) + .flatMap(Collection::stream).map(PdsLidVid::fromString).toList(); } @Override - public ResponseEntity productMemberOf( - String identifier, List fields, Integer limit, List sort, List searchAfter) - throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, BadRequestException, - AcceptFormatNotSupportedException{ + public ResponseEntity productMemberOf(String identifier, List fields, + Integer limit, String q, List sort, List searchAfter) + throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, + BadRequestException, AcceptFormatNotSupportedException, UnparsableQParamException { - try{ + try { PdsProductIdentifier pdsIdentifier = PdsProductIdentifier.fromString(identifier); PdsProductClasses productClass = resolveProductClass(pdsIdentifier); PdsLidVid lidvid = resolveIdentifierToLidvid(pdsIdentifier); List parentIds; if (productClass.isCollection()) { - parentIds = resolveLidVidsFromProductField(lidvid, "ops:Provenance/ops:parent_bundle_identifier"); + parentIds = + resolveLidVidsFromProductField(lidvid, "ops:Provenance/ops:parent_bundle_identifier"); } else if (productClass.isBasicProduct()) { - parentIds = resolveLidVidsFromProductField(lidvid, "ops:Provenance/ops:parent_collection_identifier"); + parentIds = resolveLidVidsFromProductField(lidvid, + "ops:Provenance/ops:parent_collection_identifier"); } else { - throw new BadRequestException("productMembersOf endpoint is not valid for products with Product_Class '" + - PdsProductClasses.Product_Bundle + "' (got '" + productClass + "')"); + throw new BadRequestException( + "productMembersOf endpoint is not valid for products with Product_Class '" + + PdsProductClasses.Product_Bundle + "' (got '" + productClass + "')"); } SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchFieldAnyOfIdentifiers("_id", parentIds) - .fieldsFromStrings(fields) - .paginate(limit, sort, searchAfter) - .onlyLatest() - .build(); + .applyMultipleProductsDefaults(fields, q, List.of(), limit, sort, searchAfter, true) + .matchFieldAnyOfIdentifiers("_id", parentIds).build(); SearchResponse searchResponse = - this.openSearchClient.search(searchRequest, HashMap.class); + this.openSearchClient.search(searchRequest, HashMap.class); RawMultipleProductResponse products = new RawMultipleProductResponse(searchResponse); @@ -570,34 +543,34 @@ public ResponseEntity productMemberOf( } @Override - public ResponseEntity productMemberOfOf( - String identifier, List fields, Integer limit, List sort, List searchAfter) - throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, BadRequestException, - AcceptFormatNotSupportedException{ + public ResponseEntity productMemberOfOf(String identifier, List fields, + Integer limit, String q, List sort, List searchAfter) + throws NotFoundException, UnhandledException, SortSearchAfterMismatchException, + BadRequestException, AcceptFormatNotSupportedException, UnparsableQParamException { - try{ + try { PdsProductIdentifier pdsIdentifier = PdsProductIdentifier.fromString(identifier); PdsProductClasses productClass = resolveProductClass(pdsIdentifier); PdsLidVid lidvid = resolveIdentifierToLidvid(pdsIdentifier); List parentIds; if (productClass.isBasicProduct()) { - parentIds = resolveLidVidsFromProductField(lidvid, "ops:Provenance/ops:parent_bundle_identifier"); + parentIds = + resolveLidVidsFromProductField(lidvid, "ops:Provenance/ops:parent_bundle_identifier"); } else { -// TODO: replace with enumeration of acceptable values later - throw new BadRequestException("productMembersOf endpoint is not valid for products with Product_Class '" + - PdsProductClasses.Product_Bundle + "' or '" + PdsProductClasses.Product_Collection + "' (got '" + productClass + "')"); + // TODO: replace with enumeration of acceptable values later + throw new BadRequestException( + "productMembersOf endpoint is not valid for products with Product_Class '" + + PdsProductClasses.Product_Bundle + "' or '" + PdsProductClasses.Product_Collection + + "' (got '" + productClass + "')"); } SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchFieldAnyOfIdentifiers("_id", parentIds) - .fieldsFromStrings(fields) - .paginate(limit, sort, searchAfter) - .onlyLatest() - .build(); + .applyMultipleProductsDefaults(fields, q, List.of(), limit, sort, searchAfter, true) + .matchFieldAnyOfIdentifiers("_id", parentIds).build(); SearchResponse searchResponse = - this.openSearchClient.search(searchRequest, HashMap.class); + this.openSearchClient.search(searchRequest, HashMap.class); RawMultipleProductResponse products = new RawMultipleProductResponse(searchResponse); @@ -609,26 +582,24 @@ public ResponseEntity productMemberOfOf( } @Override - // TODO: Relocate this to ClassesController once common controller code has been extracted/refactored - public ResponseEntity classList(String propertyClass, List fields, List keywords, Integer limit, String q, List sort, List searchAfter) throws Exception { + // TODO: Relocate this to ClassesController once common controller code has been + // extracted/refactored + public ResponseEntity classList(String propertyClass, List fields, + List keywords, Integer limit, String q, List sort, List searchAfter) + throws Exception { PdsProductClasses pdsProductClass; try { - pdsProductClass = PdsProductClasses.fromSwaggerName(propertyClass); + pdsProductClass = PdsProductClasses.fromSwaggerName(propertyClass); } catch (IllegalArgumentException err) { throw new BadRequestException(err.getMessage()); } SearchRequest searchRequest = new RegistrySearchRequestBuilder(this.connectionContext) - .matchProductClass(pdsProductClass) - .constrainByQueryString(q) - .addKeywordsParam(keywords) - .fieldsFromStrings(fields) - .paginate(limit, sort, searchAfter) - .onlyLatest() - .build(); + .applyMultipleProductsDefaults(fields, q, keywords, limit, sort, searchAfter, true) + .matchProductClass(pdsProductClass).build(); SearchResponse searchResponse = - this.openSearchClient.search(searchRequest, HashMap.class); + this.openSearchClient.search(searchRequest, HashMap.class); RawMultipleProductResponse products = new RawMultipleProductResponse(searchResponse); @@ -636,8 +607,66 @@ public ResponseEntity classList(String propertyClass, List field } @Override - // TODO: Relocate this to ClassesController once common controller code has been extracted/refactored + // TODO: Relocate this to ClassesController once common controller code has been + // extracted/refactored public ResponseEntity> classes() throws Exception { - return new ResponseEntity<>(Arrays.stream(PdsProductClasses.values()).map(PdsProductClasses::getSwaggerName).toList(), HttpStatusCode.valueOf(200)); + return new ResponseEntity<>( + Arrays.stream(PdsProductClasses.values()).map(PdsProductClasses::getSwaggerName).toList(), + HttpStatusCode.valueOf(200)); + } + + /** + * Resolve the appropriate enumerated user type hint from an OpenSearch Property + */ + protected PropertiesListInner.TypeEnum _resolvePropertyToEnumType(Property property) { + if (property.isBoolean()) { + return PropertiesListInner.TypeEnum.BOOLEAN; + } else if (property.isKeyword() || property.isText()) { + return PropertiesListInner.TypeEnum.STRING; + } else if (property.isDate()) { + return PropertiesListInner.TypeEnum.TIMESTAMP; + } else if (property.isInteger() || property.isLong()) { + return PropertiesListInner.TypeEnum.INTEGER; + } else if (property.isFloat() || property.isDouble()) { + return PropertiesListInner.TypeEnum.FLOAT; + } else { + return PropertiesListInner.TypeEnum.UNSUPPORTED; + } + } + + @Override + public ResponseEntity> productPropertiesList() throws Exception { + + List indexNames = this.connectionContext.getRegistryIndices(); + + GetMappingRequest getMappingRequest = new GetMappingRequest.Builder().index(indexNames).build(); + + OpenSearchIndicesClient indicesClient = this.openSearchClient.indices(); + + GetMappingResponse getMappingResponse = indicesClient.getMapping(getMappingRequest); + + Set resultIndexNames = getMappingResponse.result().keySet(); + SortedMap aggregatedMappings = new TreeMap<>(); + for (String indexName : resultIndexNames) { + Set> indexProperties = getMappingResponse.result().get(indexName).mappings().properties().entrySet(); + for (Map.Entry property : indexProperties) { + String jsonPropertyName = PdsProperty.toJsonPropertyString(property.getKey()); + Property openPropertyName = property.getValue(); + PropertiesListInner.TypeEnum propertyEnumType = _resolvePropertyToEnumType(openPropertyName); + +// No consistency-checking between duplicates, for now. TODO: add error log for mismatching duplicates + aggregatedMappings.put(jsonPropertyName, propertyEnumType); + } + } + + List apiResponseContent = aggregatedMappings.entrySet().stream().map((entry) -> { + PropertiesListInner propertyElement = new PropertiesListInner(); + propertyElement.setProperty(entry.getKey()); + propertyElement.setType(entry.getValue()); + return propertyElement; + }).toList(); + + return new ResponseEntity<>(apiResponseContent, HttpStatus.OK); } } + diff --git a/service/src/main/java/gov/nasa/pds/api/registry/controllers/RegistryApiResponseEntityExceptionHandler.java b/service/src/main/java/gov/nasa/pds/api/registry/controllers/RegistryApiResponseEntityExceptionHandler.java index 2de03b14..a530760b 100644 --- a/service/src/main/java/gov/nasa/pds/api/registry/controllers/RegistryApiResponseEntityExceptionHandler.java +++ b/service/src/main/java/gov/nasa/pds/api/registry/controllers/RegistryApiResponseEntityExceptionHandler.java @@ -2,7 +2,7 @@ import java.util.Set; - +import gov.nasa.pds.api.registry.configuration.WebMVCConfig; import gov.nasa.pds.api.registry.model.exceptions.*; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -59,7 +59,7 @@ protected ResponseEntity unhandled(UnhandledException ex, WebRequest req @ExceptionHandler(value = {AcceptFormatNotSupportedException.class}) protected ResponseEntity notAcceptable(AcceptFormatNotSupportedException ex, WebRequest request) { - Set supportedFormats = ProductsController.getFormatters().keySet(); + Set supportedFormats = WebMVCConfig.getFormatters().keySet(); String errorDescriptionSuffix = " Supported formats (in Accept header) are: " + String.join(", ", supportedFormats); @@ -69,7 +69,7 @@ protected ResponseEntity notAcceptable(AcceptFormatNotSupportedException @ExceptionHandler(value = {SortSearchAfterMismatchException.class}) protected ResponseEntity missSort(SortSearchAfterMismatchException ex, - WebRequest request) { + WebRequest request) { return genericExceptionHandler(ex, request, "", HttpStatus.BAD_REQUEST); } diff --git a/service/src/main/java/gov/nasa/pds/api/registry/model/properties/PdsProperty.java b/service/src/main/java/gov/nasa/pds/api/registry/model/properties/PdsProperty.java new file mode 100644 index 00000000..9bd23b67 --- /dev/null +++ b/service/src/main/java/gov/nasa/pds/api/registry/model/properties/PdsProperty.java @@ -0,0 +1,54 @@ +package gov.nasa.pds.api.registry.model.properties; + +import gov.nasa.pds.api.registry.exceptions.UnsupportedSearchProperty; +import gov.nasa.pds.api.registry.model.exceptions.UnhandledException; + +import static gov.nasa.pds.api.registry.model.SearchUtil.jsonPropertyToOpenProperty; +import static gov.nasa.pds.api.registry.model.SearchUtil.openPropertyToJsonProperty; + +/** + * Provides a unified interface for dealing with PDS product properties, which are named differently depending on the + * context. The canonical names used by OpenSearch use '/' as a separator, whereas users and the API URL interface + * replace these with "." + */ +public class PdsProperty { + private final String value; + + public PdsProperty(String value) { + this.value = value; + } + + /** + * Returns the property, in the format used by OpenSearch + */ + public static String toOpenPropertyString(String value) { + // TODO: replace all uses of SearchUtil.jsonPropertyToOpenProperty with PdsProperty, move conversion logic here, + // and excise SearchUtil.jsonPropertyToOpenProperty + return jsonPropertyToOpenProperty(value); + } + + public String toOpenPropertyString() { + return toOpenPropertyString(this.value); + } + + /** + * Returns the property, in the format used by the API interface (i.e. end-users) + */ + public static String toJsonPropertyString(String value) { + // TODO: replace all uses of SearchUtil.openPropertyToJsonProperty with PdsProperty, move conversion logic here, + // and excise SearchUtil.openPropertyToJsonProperty + + //TODO: Remove this stopgap once https://github.com/NASA-PDS/registry-api/issues/528 is resolved + try{ + return openPropertyToJsonProperty(value); + } catch (UnsupportedSearchProperty err) { + return value.replace('/', '.'); + } + } + + public String toJsonPropertyString() { + return toJsonPropertyString(this.value); + } + + +} diff --git a/service/src/main/java/gov/nasa/pds/api/registry/search/RegistrySearchRequestBuilder.java b/service/src/main/java/gov/nasa/pds/api/registry/search/RegistrySearchRequestBuilder.java index 66e3e065..ab1ed850 100644 --- a/service/src/main/java/gov/nasa/pds/api/registry/search/RegistrySearchRequestBuilder.java +++ b/service/src/main/java/gov/nasa/pds/api/registry/search/RegistrySearchRequestBuilder.java @@ -106,6 +106,39 @@ public BoolQuery.Builder getQueryBuilder() { return this.queryBuilder; } + /** + * Applies a common set of constraints and other build options which generally apply to any endpoint which queries + * OpenSearch for a result-set of multiple products. + * @param includeFieldNames - which properties to include in the results (JSON format, not OpenSearch format) + * @param queryString - a querystring (q=) to constrain the result-set by + * @param keywords - a set of keyword matches to + * @param pageSize - the page size to use for pagination + * @param sortFieldNames - the fields by which results are sorted (ascending), from highest to lowest priority + * @param searchAfterFieldValues - the values corresponding to the sort fields, for pagination + * @param excludeSupersededProducts - whether to exclude superseded products from the result set + */ + public RegistrySearchRequestBuilder applyMultipleProductsDefaults( + List includeFieldNames, + String queryString, + List keywords, + Integer pageSize, + List sortFieldNames, + List searchAfterFieldValues, + Boolean excludeSupersededProducts + ) throws UnparsableQParamException, SortSearchAfterMismatchException { + this + .fieldsFromStrings(includeFieldNames) + .constrainByQueryString(queryString) + .addKeywordsParam(keywords) + .paginate(pageSize, sortFieldNames, searchAfterFieldValues); + + if (excludeSupersededProducts) { + this.excludeSupersededProducts(); + } + + return this; + } + public SearchRequest build() { this.query(this.queryBuilder.build().toQuery()); this.trackTotalHits(t -> t.enabled(true)); @@ -325,7 +358,7 @@ public RegistrySearchRequestBuilder addKeywordsParam(List keywords) { * N.B. this does *not* mean the latest version which satisfies other constraints, so application of this constraint * can result in no hits being returned despite valid results existing. */ - public RegistrySearchRequestBuilder onlyLatest() { + public RegistrySearchRequestBuilder excludeSupersededProducts() { ExistsQuery supersededByExists = new ExistsQuery.Builder() .field("ops:Provenance/ops:superseded_by") diff --git a/service/src/main/resources/application.properties b/service/src/main/resources/application.properties index 1a253456..2cadcf58 100644 --- a/service/src/main/resources/application.properties +++ b/service/src/main/resources/application.properties @@ -10,7 +10,7 @@ springdoc.swagger-ui.tagsSorter=alpha springdoc.api-docs.path=/api-docs springdoc.api-docs.enabled=true -springdoc.packagesToScan=gov.nasa.pds.api.registry.controller +springdoc.packagesToScan=gov.nasa.pds.api.registry.controllers springdoc.pathsToMatch=/** server.forward-headers-strategy=framework management.endpoints.web.exposure.include=* @@ -20,6 +20,8 @@ debug=true logging.level.root = DEBUG log4j.logger.org.springframework=DEBUG logging.level.gov.nasa.pds.api.registry.opensearch = DEBUG +logging.level.org.apache.hc.client5.http.wire = INFO +logging.level.org.apache.http.wire = INFO server.ssl.enabled=false server.ssl.key-alias=registry @@ -32,6 +34,9 @@ openSearch.host=localhost:9200 openSearch.registryIndex=registry openSearch.registryRefIndex=registry-refs openSearch.timeOutSeconds=60 +# , separated list of the prefixes used in the opensearch indices, +# if none, keep this configuration empty. +openSearch.disciplineNodes= openSearch.CCSEnabled=true openSearch.username=admin openSearch.password=admin diff --git a/service/src/test/java/gov/nasa/pds/api/registry/configuration/WebMVCConfigTest.java b/service/src/test/java/gov/nasa/pds/api/registry/configuration/WebMVCConfigTest.java new file mode 100644 index 00000000..5c3abf63 --- /dev/null +++ b/service/src/test/java/gov/nasa/pds/api/registry/configuration/WebMVCConfigTest.java @@ -0,0 +1,114 @@ +package gov.nasa.pds.api.registry.configuration; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import gov.nasa.pds.api.registry.model.api_responses.ProductBusinessLogic; +import gov.nasa.pds.api.registry.model.exceptions.AcceptFormatNotSupportedException; + +class WebMVCConfigTest { + + @BeforeAll + static void setUpBeforeClass() throws Exception { + + + } + + @Test + void selectFormatterClassFromSingleFormatSuccessfulTest() { + + String format = "text/html"; + String expectedFormatterClassName = + "gov.nasa.pds.api.registry.model.api_responses.PdsProductBusinessObject"; + String foundFormatterClassName; + + try { + Class formatter = WebMVCConfig.selectFormatterClass(format); + + foundFormatterClassName = formatter.getName(); + assertEquals(expectedFormatterClassName, foundFormatterClassName); + + } catch (AcceptFormatNotSupportedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + + @Test + void selectFormatterClassFromSingleFormatFailedTest() { + + String format = "text/htmm"; + + Exception exception = assertThrows(AcceptFormatNotSupportedException.class, () -> { + WebMVCConfig.selectFormatterClass(format); + }); + + String expectedMessage = "None of the format(s) text/htmm is supported."; + String actualMessage = exception.getMessage(); + + assertTrue(actualMessage.contains(expectedMessage)); + + } + + @Test + void selectFormatterClassFromMultipleFormatSuccessfulTest() { + + String format = "text/ms+word,text/html"; + String expectedFormatterClassName = + "gov.nasa.pds.api.registry.model.api_responses.PdsProductBusinessObject"; + String foundFormatterClassName; + + try { + Class formatter = WebMVCConfig.selectFormatterClass(format); + + foundFormatterClassName = formatter.getName(); + assertEquals(expectedFormatterClassName, foundFormatterClassName); + + } catch (AcceptFormatNotSupportedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + + @Test + void selectFormatterClassFromMultipleFormatExtraSpacesSuccessfulTest() { + + String format = "text/ms+word,text/html ,anything/something"; + String expectedFormatterClassName = + "gov.nasa.pds.api.registry.model.api_responses.PdsProductBusinessObject"; + String foundFormatterClassName; + + try { + Class formatter = WebMVCConfig.selectFormatterClass(format); + + foundFormatterClassName = formatter.getName(); + assertEquals(expectedFormatterClassName, foundFormatterClassName); + + } catch (AcceptFormatNotSupportedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + + @Test + void selectFormatterClassFromMultipleFormatFailedTest() { + + String format = "text/htmm,car/porsche+911"; + + Exception exception = assertThrows(AcceptFormatNotSupportedException.class, () -> { + WebMVCConfig.selectFormatterClass(format); + }); + + + String expectedMessage = "None of the format(s) text/htmm,car/porsche+911 is supported."; + String actualMessage = exception.getMessage(); + + assertTrue(actualMessage.contains(expectedMessage)); + + } + + +} diff --git a/terraform/ecs.tf b/terraform/ecs.tf index bc7dd0d0..fea7a080 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -1,3 +1,75 @@ +resource "aws_lb" "registry-api-lb" { + name = "registry-api-lb-new" + internal = false + load_balancer_type = "application" + security_groups = var.aws_fg_security_groups + subnets = var.aws_lb_subnets + + enable_deletion_protection = false + + access_logs { + bucket = var.aws_s3_bucket_logs_id + prefix = "registry-api-lb" + enabled = true + } + + tags = { + Alfa = var.node_name_abbr + Bravo = var.venue + Charlie = "registry" + } +} + +resource "aws_ssm_parameter" "load_balancer_domain" { + name = "/pds/registry/load-balancer-domain" + type = "String" + overwrite = true + value = aws_lb.registry-api-lb.dns_name +} + +resource "aws_lb_target_group" "pds-registry-api-target-group" { + name = "pds-${var.venue}-registry-tgt" + port = 80 + protocol = "HTTP" + target_type = "ip" + vpc_id = var.aws_fg_vpc + + health_check { + enabled = true + path = "/healthcheck" + matcher = "200" + interval = 300 + } +} + +resource "aws_lb_listener" "registry-api-ld-listener" { + load_balancer_arn = aws_lb.registry-api-lb.arn + port = 80 + protocol = "HTTP" + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.pds-registry-api-target-group.arn + } +} + +resource "aws_lb_listener_rule" "pds-registry-forward-rule" { + listener_arn = aws_lb_listener.registry-api-ld-listener.arn + + action { + type = "forward" + target_group_arn = aws_lb_target_group.pds-registry-api-target-group.arn + } + + # no condition for now + # TODO add condition so that the same load balancer can be + # used for multiple back-end service + condition { + path_pattern { + values = ["/*"] + } + } +} + # Define the cluster resource "aws_ecs_cluster" "pds-registry-api-ecs" { name = "pds-${var.venue}-registry-api-ecs" @@ -26,33 +98,6 @@ resource "aws_cloudwatch_log_group" "pds-registry-log-group" { } } -# The main service. -resource "aws_ecs_service" "pds-registry-reg-service" { - name = "pds-${var.venue}-registry-api-service" - task_definition = aws_ecs_task_definition.pds-registry-ecs-task.arn - cluster = aws_ecs_cluster.pds-registry-api-ecs.id - launch_type = "FARGATE" - - desired_count = 1 - - load_balancer { - target_group_arn = aws_lb_target_group.pds-registry-api-target-group.arn - container_name = "pds-${var.venue}-reg-container" - container_port = "80" - } - - network_configuration { - assign_public_ip = false - security_groups = var.aws_fg_security_groups - subnets = var.aws_fg_subnets - } - - tags = { - Alfa = var.node_name_abbr - Bravo = var.venue - Charlie = "registry" - } -} # The task definition for app. resource "aws_ecs_task_definition" "pds-registry-ecs-task" { @@ -114,19 +159,26 @@ EOF } -resource "aws_lb" "registry-api-lb" { - name = "registry-api-lb-new" - internal = false - load_balancer_type = "application" - security_groups = var.aws_fg_security_groups - subnets = var.aws_fg_subnets - enable_deletion_protection = false +# The main service. +resource "aws_ecs_service" "pds-registry-reg-service" { + name = "pds-${var.venue}-registry-api-service" + task_definition = aws_ecs_task_definition.pds-registry-ecs-task.arn + cluster = aws_ecs_cluster.pds-registry-api-ecs.id + launch_type = "FARGATE" - access_logs { - bucket = var.aws_s3_bucket_logs_id - prefix = "registry-api-lb" - enabled = true + desired_count = 1 + + load_balancer { + target_group_arn = aws_lb_target_group.pds-registry-api-target-group.arn + container_name = "pds-${var.venue}-reg-container" + container_port = "80" + } + + network_configuration { + assign_public_ip = false + security_groups = var.aws_fg_security_groups + subnets = var.aws_fg_subnets } tags = { @@ -136,45 +188,3 @@ resource "aws_lb" "registry-api-lb" { } } -resource "aws_lb_target_group" "pds-registry-api-target-group" { - name = "pds-${var.venue}-registry-tgt" - port = 80 - protocol = "HTTP" - target_type = "ip" - vpc_id = var.aws_fg_vpc - - health_check { - enabled = true - path = "/healthcheck" - matcher = "200" - interval = 300 - } -} - -resource "aws_lb_listener" "registry-api-ld-listener" { - load_balancer_arn = aws_lb.registry-api-lb.arn - port = 80 - protocol = "HTTP" - default_action { - type = "forward" - target_group_arn = aws_lb_target_group.pds-registry-api-target-group.arn - } -} - -resource "aws_lb_listener_rule" "pds-registry-forward-rule" { - listener_arn = aws_lb_listener.registry-api-ld-listener.arn - - action { - type = "forward" - target_group_arn = aws_lb_target_group.pds-registry-api-target-group.arn - } - - # no condition for now - # TODO add condition so that the same load balancer can be - # used for multiple back-end service - condition { - path_pattern { - values = ["/*"] - } - } -} diff --git a/terraform/variables.tf b/terraform/variables.tf index 5d02b4f7..863a9315 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -36,6 +36,11 @@ variable "aws_fg_subnets" { type = list(string) } +variable "aws_lb_subnets" { + description = "AWS Subnets for the load balancer" + type = list(string) +} + variable "ecs_task_role" { description = "ECS task role" }