diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/APIService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/APIService.java index 89fa910c3d..86d7179032 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/APIService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/model/APIService.java @@ -12,9 +12,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.zowe.apiml.config.ApiInfo; import java.io.Serializable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,11 +65,8 @@ public class APIService implements Serializable { @Schema(description = "The SSO support for all instances") private boolean ssoAllInstances; - @Schema(description = "The API ID for this service") - private Map apiId; - - @Schema(description = "The Gateway URLs used within this service") - private Map gatewayUrls; + @Schema(description = "The API information for each API ID for this service") + private Map apis = new HashMap<>(); // NOSONAR private List instances = new ArrayList<>(); @@ -77,7 +76,7 @@ private APIService(String serviceId) { } public static class Builder { - private APIService apiService; + private final APIService apiService; public Builder(String serviceId) { apiService = new APIService(serviceId); @@ -129,18 +128,13 @@ public Builder sso(boolean sso) { return this; } - public Builder apiId(Map apiId) { - apiService.apiId = apiId; - return this; - } - - public Builder gatewayUrls(Map gatewayUrls) { - apiService.gatewayUrls = gatewayUrls; + public Builder instanceId(String id) { + apiService.instances.add(id); return this; } - public Builder instanceId(String id) { - apiService.instances.add(id); + public Builder apis(Map apis) { + apiService.apis = apis; return this; } diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java index 60324c5f02..040029eef8 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyService.java @@ -31,7 +31,6 @@ import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; import static java.util.stream.Collectors.toList; import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; @@ -43,6 +42,8 @@ @Service public class CachedProductFamilyService { + private static final String DEFAULT_APIINFO_KEY = "default"; + @InjectApimlLogger private final ApimlLogger apimlLog = ApimlLogger.empty(); @@ -130,7 +131,7 @@ public APIContainer saveContainerFromInstance(String productFamilyId, InstanceIn } else { Set apiServices = container.getServices(); APIService service = createAPIServiceFromInstance(instanceInfo); - + // Verify whether already exists if (apiServices.contains(service)) { apiServices.stream() @@ -139,7 +140,7 @@ public APIContainer saveContainerFromInstance(String productFamilyId, InstanceIn if (!existingService.getInstances().contains(instanceInfo.getInstanceId())) { existingService.getInstances().add(instanceInfo.getInstanceId()); } - }); // If the instance is in list, do nothing otherwise + }); // If the instance is in list, do nothing otherwise } else { apiServices.add(service); } @@ -166,7 +167,7 @@ public APIContainer saveContainerFromInstance(String productFamilyId, InstanceIn * 1) it will remove the whole APIContainer (Tile) if there is no instance of any service remaining * 2) Remove the service from the containe if there is no instance of service remaining * 3) Remove instance from the service - * + * * @param removedInstanceFamilyId the product family id of the container * @param removedInstance the service instance */ @@ -219,7 +220,7 @@ public void calculateContainerServiceValues(APIContainer apiContainer) { boolean isSso = servicesCount > 0; for (APIService apiService : apiContainer.getServices()) { if (update(apiService)) { - activeServicesCount ++; + activeServicesCount++; } isSso &= apiService.isSsoAllInstances(); } @@ -333,22 +334,19 @@ private APIService createAPIServiceFromInstance(InstanceInfo instanceInfo) { String instanceHomePage = getInstanceHomePageUrl(instanceInfo); String apiBasePath = getApiBasePath(instanceInfo); - Map apiId = new HashMap<>(); - Map gatewayUrls = new HashMap<>(); + Map apiInfoById = new HashMap<>(); + try { - apiId = metadataParser.parseApiInfo(instanceInfo.getMetadata()).stream().filter(apiInfo -> apiInfo.getApiId() != null).collect( - Collectors.toMap( - apiInfo -> (apiInfo.getMajorVersion() < 0) ? "default" : apiInfo.getApiId() + " v" + apiInfo.getVersion(), - ApiInfo::getApiId - ) - ); - - gatewayUrls = metadataParser.parseApiInfo(instanceInfo.getMetadata()).stream().filter(apiInfo -> apiInfo.getApiId() != null).collect( - Collectors.toMap( - apiInfo -> (apiInfo.getMajorVersion() < 0) ? "default" : apiInfo.getApiId() + " v" + apiInfo.getVersion(), - ApiInfo::getGatewayUrl - ) - ); + List apiInfoList = metadataParser.parseApiInfo(instanceInfo.getMetadata()); + apiInfoList.stream().filter(apiInfo -> apiInfo.getApiId() != null).forEach(apiInfo -> { + String id = (apiInfo.getMajorVersion() < 0) ? DEFAULT_APIINFO_KEY : apiInfo.getApiId() + " v" + apiInfo.getVersion(); + apiInfoById.put(id, apiInfo); + }); + + if (!apiInfoById.containsKey(DEFAULT_APIINFO_KEY)) { + ApiInfo defaultApiInfo = apiInfoList.stream().filter(ApiInfo::isDefaultApi).findFirst().orElse(null); + apiInfoById.put(DEFAULT_APIINFO_KEY, defaultApiInfo); + } } catch (Exception ex) { log.info("createApiServiceFromInstance#incorrectVersions {}", ex.getMessage()); } @@ -361,8 +359,7 @@ private APIService createAPIServiceFromInstance(InstanceInfo instanceInfo) { .homePageUrl(instanceHomePage) .basePath(apiBasePath) .sso(isSso(instanceInfo)) - .apiId(apiId) - .gatewayUrls(gatewayUrls) + .apis(apiInfoById) .instanceId(instanceInfo.getInstanceId()) .build(); } diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index d659ebb1e6..bc3232ba1c 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -144,6 +144,13 @@ eureka: version: 1.0.0 gatewayUrl: api/v1 swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v3/api-docs + codeSnippet: + - endpoint: "/greetings" + codeBlock: | + ``` + System. out. println("Hello, World!"); + ``` + language: java service: title: API Catalog diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java index d202ba82cf..c4a7a91ec9 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java @@ -61,7 +61,7 @@ class WhenAllContainersAreRequested { @Test void thenReturnNoContent() { given(cachedProductFamilyService.getAllContainers()).willReturn(null); - + RestAssuredMockMvc.given(). when(). get(pathToContainers). @@ -69,7 +69,7 @@ void thenReturnNoContent() { statusCode(HttpStatus.NO_CONTENT.value()); } } - + @Nested class WhenSpecificContainerRequested { @Test @@ -103,8 +103,8 @@ void prepareApplications() { apiVersions = Arrays.asList("1.0.0", "2.0.0"); given(cachedServicesService.getService("service1")).willReturn(service1); - given(cachedApiDocService.getDefaultApiDocForService("service1")).willReturn("service1"); - given(cachedApiDocService.getApiVersionsForService("service1")).willReturn(apiVersions); + given(cachedApiDocService.getDefaultApiDocForService("service1")).willReturn("service1"); + given(cachedApiDocService.getApiVersionsForService("service1")).willReturn(apiVersions); given(cachedServicesService.getService("service2")).willReturn(service2); given(cachedApiDocService.getDefaultApiDocForService("service2")).willReturn("service2"); @@ -193,7 +193,7 @@ private void assertThereIsOneContainer(ResponseEntity> contai } } - + // =========================================== Helper Methods =========================================== @@ -209,7 +209,7 @@ private List createContainers() { .homePageUrl("home") .basePath("base") .sso(false) - .apiId(Collections.emptyMap()) + .apis(Collections.emptyMap()) .build(); services.add(service); @@ -221,7 +221,7 @@ private List createContainers() { .homePageUrl("home") .basePath("base") .sso(false) - .apiId(Collections.emptyMap()) + .apis(Collections.emptyMap()) .build(); services.add(service); diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java index 843a399bbf..4b17845904 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/services/cached/CachedProductFamilyServiceTest.java @@ -83,7 +83,7 @@ void prepareInstances() throws URLTransformationException { metadata.put(SERVICE_DESCRIPTION, "sDescription"); instance = servicesBuilder.createInstance("service1", InstanceInfo.InstanceStatus.UP, metadata); - Map updatedMetadata = new HashMap(); + Map updatedMetadata = new HashMap<>(); updatedMetadata.put(CATALOG_ID, "demoapp"); updatedMetadata.put(CATALOG_TITLE, "Title2"); updatedMetadata.put(CATALOG_DESCRIPTION, "Description2"); @@ -101,7 +101,7 @@ void prepareInstances() throws URLTransformationException { @Nested class GivenInstanceIsNotInCache { @Test - void createNew() throws URLTransformationException { + void createNew() { APIContainer originalContainer = underTest.saveContainerFromInstance("demoapp", instance); List lsContainer = underTest.getRecentlyUpdatedContainers(); @@ -112,7 +112,7 @@ void createNew() throws URLTransformationException { @Nested class GivenInstanceIsInTheCache { @Test - void update() throws InterruptedException, URLTransformationException { + void update() throws InterruptedException { APIContainer originalContainer = underTest.saveContainerFromInstance("demoapp", instance); Calendar createdTimestamp = originalContainer.getLastUpdatedTimestamp(); @@ -151,7 +151,7 @@ private void assertThatMetadataAreCorrect(APIContainer result, Map createContainers() { .homePageUrl("home") .basePath("base") .sso(false) - .apiId(Collections.emptyMap()) + .apis(Collections.emptyMap()) .build(); services.add(service); @@ -184,7 +184,7 @@ private List createContainers() { .homePageUrl("home") .basePath("base") .sso(false) - .apiId(Collections.emptyMap()) + .apis(Collections.emptyMap()) .build(); services.add(service); diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java index 253afde7a6..3b265bef6f 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/util/ContainerServiceMockUtil.java @@ -115,7 +115,7 @@ private APIService addApiService(String serviceId, .homePageUrl("home") .basePath("base") .sso(false) - .apiId(Collections.emptyMap()) + .apis(Collections.emptyMap()) .build(); services.add(service); allServices.add(service); diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.jsx b/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.jsx index f9374a9bcd..74f253ef54 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.jsx @@ -16,9 +16,9 @@ export default class InstanceInfo extends Component { render() { const { selectedService, selectedVersion } = this.props; - const apiId = - selectedService.apiId[selectedVersion || selectedService.defaultApiVersion] || - selectedService.apiId.default; + const apiInfo = + selectedService.apis[selectedVersion || selectedService.defaultApiVersion] || selectedService.apis.default; + const { apiId } = apiInfo; return (
@@ -31,11 +31,7 @@ export default class InstanceInfo extends Component {
- + {/* eslint-disable-next-line jsx-a11y/label-has-for */} diff --git a/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.test.jsx b/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.test.jsx index a1342660d4..10b915d0c3 100644 --- a/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.test.jsx +++ b/api-catalog-ui/frontend/src/components/ServiceTab/InstanceInfo.test.jsx @@ -20,8 +20,8 @@ const selectedService = { describe('>>> InstanceInfo component tests', () => { it('Service with version v1', () => { - selectedService.apiId = { - v1: 'zowe.apiml.gateway', + selectedService.apis = { + v1: { apiId: 'zowe.apiml.gateway' }, }; const selectService = jest.fn(); const instanceInfo = shallow( @@ -35,8 +35,8 @@ describe('>>> InstanceInfo component tests', () => { }); it('No selected version, use defaultApiVersion', () => { - selectedService.apiId = { - v1: 'zowe.apiml.gateway', + selectedService.apis = { + v1: { apiId: 'zowe.apiml.gateway' }, }; selectedService.defaultApiVersion = ['v1']; const selectService = jest.fn(); @@ -46,8 +46,8 @@ describe('>>> InstanceInfo component tests', () => { }); it('No selected version and not set defaultApiVersion use key default', () => { - selectedService.apiId = { - default: 'zowe.apiml.gateway', + selectedService.apis = { + default: { apiId: 'zowe.apiml.gateway' }, }; selectedService.defaultApiVersion = null; const selectService = jest.fn(); diff --git a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.test.jsx b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.test.jsx index 55c754ddfc..d56c5067af 100644 --- a/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.test.jsx +++ b/api-catalog-ui/frontend/src/components/Swagger/SwaggerUI.test.jsx @@ -70,8 +70,8 @@ describe('>>> Swagger component tests', () => { openapi: '3.0.0', servers: [{ url: `https://bad.com${endpoint}` }], }), - apiId: { - default: 'enabler', + apis: { + default: { apiId: 'enabler' }, }, }; @@ -97,8 +97,8 @@ describe('>>> Swagger component tests', () => { openapi: '3.0.0', servers: [{ url: `https://bad.com${endpoint1}` }], }), - apiId: { - default: 'oldenabler', + apis: { + default: { apiId: 'oldenabler' }, }, }; @@ -114,8 +114,8 @@ describe('>>> Swagger component tests', () => { openapi: '3.0.0', servers: [{ url: `https://bad.com${endpoint2}` }], }), - apiId: { - default: 'oldenabler', + apis: { + default: { apiId: 'oldenabler' }, }, }; diff --git a/common-service-core/src/main/java/org/zowe/apiml/config/ApiInfo.java b/common-service-core/src/main/java/org/zowe/apiml/config/ApiInfo.java index b24075c0fb..90f425c197 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/config/ApiInfo.java +++ b/common-service-core/src/main/java/org/zowe/apiml/config/ApiInfo.java @@ -22,6 +22,8 @@ import lombok.experimental.SuperBuilder; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * Represents one API provided by a service @@ -38,6 +40,7 @@ public ApiInfo(String apiId, String gatewayUrl, String version, String swaggerUr this.version = version; this.swaggerUrl = swaggerUrl; this.documentationUrl = documentationUrl; + this.codeSnippet = new ArrayList<>(); } /** @@ -62,10 +65,17 @@ public ApiInfo(String apiId, String gatewayUrl, String version, String swaggerUr private String swaggerUrl; private String documentationUrl; + @Builder.Default + private List codeSnippet = new ArrayList<>(); + @JsonDeserialize(using = StringToBooleanDeserializer.class) @Builder.Default private boolean isDefaultApi = false; + public void addCodeSnippet(CodeSnippet newCodeSnippet) { + this.codeSnippet.add(newCodeSnippet); + } + @JsonIgnore public int getMajorVersion() { if (version == null) { diff --git a/common-service-core/src/main/java/org/zowe/apiml/config/CodeSnippet.java b/common-service-core/src/main/java/org/zowe/apiml/config/CodeSnippet.java new file mode 100644 index 0000000000..8da61e9eae --- /dev/null +++ b/common-service-core/src/main/java/org/zowe/apiml/config/CodeSnippet.java @@ -0,0 +1,35 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * Represents one code snippet provided by a service + */ +@NoArgsConstructor +@Data +@SuperBuilder +public class CodeSnippet { + + @JsonProperty(required = true) + private String endpoint; + private String codeBlock; + private String language; + + public CodeSnippet(String endpoint, String codeBlock, String language) { + this.endpoint = endpoint; + this.codeBlock = codeBlock; + this.language = language; + } +} diff --git a/common-service-core/src/main/java/org/zowe/apiml/constants/EurekaMetadataDefinition.java b/common-service-core/src/main/java/org/zowe/apiml/constants/EurekaMetadataDefinition.java index ff0075aaed..1954efbe8a 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/constants/EurekaMetadataDefinition.java +++ b/common-service-core/src/main/java/org/zowe/apiml/constants/EurekaMetadataDefinition.java @@ -39,6 +39,11 @@ private EurekaMetadataDefinition() { public static final String API_INFO_DOCUMENTATION_URL = "documentationUrl"; public static final String API_INFO_IS_DEFAULT = "defaultApi"; + public static final String CODE_SNIPPET = "codeSnippet"; + public static final String CODE_SNIPPET_ENDPOINT = "endpoint"; + public static final String CODE_SNIPPET_CODE_BLOCK = "codeBlock"; + public static final String CODE_SNIPPET_LANGUAGE = "language"; + public static final String AUTHENTICATION_SCHEME = "apiml.authentication.scheme"; public static final String AUTHENTICATION_APPLID = "apiml.authentication.applid"; public static final String AUTHENTICATION_SSO = "apiml.authentication.sso"; diff --git a/common-service-core/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParser.java b/common-service-core/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParser.java index ed1dc9b0b0..c36f452986 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParser.java +++ b/common-service-core/src/main/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParser.java @@ -9,10 +9,12 @@ */ package org.zowe.apiml.eurekaservice.client.util; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.BooleanUtils; import org.zowe.apiml.auth.Authentication; import org.zowe.apiml.auth.AuthenticationSchemes; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.CodeSnippet; import org.zowe.apiml.exception.MetadataValidationException; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; @@ -29,9 +31,10 @@ import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; public class EurekaMetadataParser { - private static final String THREE_STRING_MERGE_FORMAT = "%s.%s.%s"; + private static final String FIVE_STRING_MERGE_FORMAT = "%s.%s.%s.%s.%s"; + private final ObjectMapper objectMapper = new ObjectMapper(); private final ApimlLogger apimlLog = ApimlLogger.of(EurekaMetadataParser.class, YamlMessageServiceInstance.getInstance()); private final AuthenticationSchemes schemes = new AuthenticationSchemes(); @@ -41,45 +44,57 @@ public class EurekaMetadataParser { * @param eurekaMetadata the eureka metadata * @return ApiInfo list */ - public List parseApiInfo(Map eurekaMetadata) { - Map apiInfo = new HashMap<>(); - + Map> collectedApiInfoEntries = new HashMap<>(); eurekaMetadata.entrySet() .stream() .filter(metadata -> metadata.getKey().startsWith(API_INFO)) .forEach(metadata -> { String[] keys = metadata.getKey().split("\\."); - if (keys.length == 4) { - apiInfo.putIfAbsent(keys[2], new ApiInfo()); - ApiInfo api = apiInfo.get(keys[2]); - switch (keys[3]) { - case API_INFO_API_ID: - api.setApiId(metadata.getValue()); - break; - case API_INFO_GATEWAY_URL: - api.setGatewayUrl(metadata.getValue()); - break; - case API_INFO_VERSION: - api.setVersion(metadata.getValue()); - break; - case API_INFO_SWAGGER_URL: - api.setSwaggerUrl(metadata.getValue()); - break; - case API_INFO_DOCUMENTATION_URL: - api.setDocumentationUrl(metadata.getValue()); - break; - case API_INFO_IS_DEFAULT: - api.setDefaultApi(Boolean.parseBoolean(metadata.getValue())); - break; - default: - apimlLog.log("org.zowe.apiml.common.apiInfoParsingError", metadata); - break; + if (keys.length >= 4) { // at least 4 keys split by '.' if is an ApiInfo config entry + String entryIndex = keys[2]; + String entryKey = keys[3]; + collectedApiInfoEntries.putIfAbsent(entryIndex, new HashMap<>()); + Map apiInfoEntries = collectedApiInfoEntries.get(entryIndex); + + if (metadata.getKey().contains(CODE_SNIPPET) && keys.length >= 6) { + String codeSnippetEntryIndex = keys[4]; + String codeSnippetChildKey = keys[5]; + + apiInfoEntries.putIfAbsent(entryKey, new HashMap<>()); + + @SuppressWarnings("unchecked") + Map> codeSnippetMap = (Map>) apiInfoEntries.get(entryKey); + codeSnippetMap.putIfAbsent(codeSnippetEntryIndex, new HashMap<>()); + + Map codeSnippetChildEntry = codeSnippetMap.get(codeSnippetEntryIndex); + codeSnippetChildEntry.put(codeSnippetChildKey, metadata.getValue()); + codeSnippetMap.put(codeSnippetEntryIndex, codeSnippetChildEntry); + apiInfoEntries.put(entryKey, codeSnippetMap); + } else { + apiInfoEntries.put(entryKey, metadata.getValue()); } + collectedApiInfoEntries.put(entryIndex, apiInfoEntries); } }); - return new ArrayList<>(apiInfo.values()); + List apiInfoList = new ArrayList<>(); + collectedApiInfoEntries.values().forEach(fields -> { + try { + if (fields.containsKey(CODE_SNIPPET)) { + + @SuppressWarnings("unchecked") + Map> codeSnippetMap = (Map>) fields.get(CODE_SNIPPET); + List> codeSnippetList = new ArrayList<>(codeSnippetMap.values()); + + fields.put(CODE_SNIPPET, codeSnippetList); + } + apiInfoList.add(objectMapper.convertValue(fields, ApiInfo.class)); + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.common.apiInfoParsingError", fields); + } + }); + return apiInfoList; } /** @@ -95,7 +110,6 @@ public RoutedServices parseRoutes(Map eurekaMetadata) { return routes; } - /** * Parse eureka metadata and return list of routes * @@ -204,6 +218,15 @@ public static Map generateMetadata(String serviceId, ApiInfo api metadata.put(String.format(THREE_STRING_MERGE_FORMAT, API_INFO, encodedGatewayUrl, API_INFO_DOCUMENTATION_URL), apiInfo.getDocumentationUrl()); } + List codeSnippets = apiInfo.getCodeSnippet(); + if (codeSnippets != null && !codeSnippets.isEmpty()) { + for (int i = 0; i < codeSnippets.size(); i++) { + metadata.put(String.format(FIVE_STRING_MERGE_FORMAT, API_INFO, encodedGatewayUrl, CODE_SNIPPET, i, CODE_SNIPPET_ENDPOINT), codeSnippets.get(i).getEndpoint()); + metadata.put(String.format(FIVE_STRING_MERGE_FORMAT, API_INFO, encodedGatewayUrl, CODE_SNIPPET, i, CODE_SNIPPET_CODE_BLOCK), codeSnippets.get(i).getCodeBlock()); + metadata.put(String.format(FIVE_STRING_MERGE_FORMAT, API_INFO, encodedGatewayUrl, CODE_SNIPPET, i, CODE_SNIPPET_LANGUAGE), codeSnippets.get(i).getLanguage()); + } + } + metadata.put(String.format(THREE_STRING_MERGE_FORMAT, API_INFO, encodedGatewayUrl, API_INFO_IS_DEFAULT), String.valueOf(apiInfo.isDefaultApi())); return metadata; @@ -219,11 +242,11 @@ private static void validateUrl(String url, Supplier exceptionSupplier) public Authentication parseAuthentication(Map eurekaMetadata) { return Authentication.builder() - .applid(eurekaMetadata.get(AUTHENTICATION_APPLID)) - .scheme(schemes.map(eurekaMetadata.get(AUTHENTICATION_SCHEME))) - .headers(eurekaMetadata.get(AUTHENTICATION_HEADERS)) - .supportsSso(BooleanUtils.toBooleanObject(eurekaMetadata.get(AUTHENTICATION_SSO))) - .build(); + .applid(eurekaMetadata.get(AUTHENTICATION_APPLID)) + .scheme(schemes.map(eurekaMetadata.get(AUTHENTICATION_SCHEME))) + .headers(eurekaMetadata.get(AUTHENTICATION_HEADERS)) + .supportsSso(BooleanUtils.toBooleanObject(eurekaMetadata.get(AUTHENTICATION_SSO))) + .build(); } } diff --git a/common-service-core/src/test/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParserTest.java b/common-service-core/src/test/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParserTest.java index 799a293b42..11ffdfd7be 100644 --- a/common-service-core/src/test/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParserTest.java +++ b/common-service-core/src/test/java/org/zowe/apiml/eurekaservice/client/util/EurekaMetadataParserTest.java @@ -9,9 +9,11 @@ */ package org.zowe.apiml.eurekaservice.client.util; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.zowe.apiml.auth.Authentication; import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.config.CodeSnippet; import org.zowe.apiml.exception.MetadataValidationException; import org.zowe.apiml.product.routing.RoutedService; import org.zowe.apiml.product.routing.RoutedServices; @@ -28,29 +30,65 @@ class EurekaMetadataParserTest { private final EurekaMetadataParser eurekaMetadataParser = new EurekaMetadataParser(); - @Test - void testParseApiInfo() { - Map metadata = new HashMap<>(); - metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, "gatewayUrl"); - metadata.put(API_INFO + ".2." + API_INFO_GATEWAY_URL, "gatewayUrl2"); - metadata.put(API_INFO + ".2." + API_INFO_SWAGGER_URL, "swagger"); - metadata.put(API_INFO + ".2." + API_INFO_DOCUMENTATION_URL, "doc"); - metadata.put(API_INFO + ".1." + API_INFO_API_ID, "zowe.apiml.test"); - metadata.put(API_INFO + ".1." + API_INFO_VERSION, "1.0.0"); - metadata.put(API_INFO + ".1." + API_INFO_IS_DEFAULT, "true"); - metadata.put(API_INFO + ".1.badArgument", "garbage"); - - List info = eurekaMetadataParser.parseApiInfo(metadata); - - assertEquals(2, info.size()); - assertEquals("gatewayUrl", info.get(0).getGatewayUrl()); - assertEquals("zowe.apiml.test", info.get(0).getApiId()); - assertEquals("1.0.0", info.get(0).getVersion()); - assertTrue(info.get(0).isDefaultApi()); - assertEquals("gatewayUrl2", info.get(1).getGatewayUrl()); - assertEquals("swagger", info.get(1).getSwaggerUrl()); - assertEquals("doc", info.get(1).getDocumentationUrl()); - assertFalse(info.get(1).isDefaultApi()); + @Nested + class WhenParseApiInfo { + @Test + void givenTwoEntries_thenReturnTwoInstances() { + Map metadata = new HashMap<>(); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".1." + CODE_SNIPPET_ENDPOINT, "endpoint"); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".1." + CODE_SNIPPET_LANGUAGE, "java"); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".1." + CODE_SNIPPET_CODE_BLOCK, "codeblock"); + metadata.put(API_INFO + ".1." + API_INFO_GATEWAY_URL, "gatewayUrl"); + metadata.put(API_INFO + ".2." + API_INFO_GATEWAY_URL, "gatewayUrl2"); + metadata.put(API_INFO + ".2." + API_INFO_SWAGGER_URL, "swagger"); + metadata.put(API_INFO + ".2." + API_INFO_DOCUMENTATION_URL, "doc"); + metadata.put(API_INFO + ".1." + API_INFO_API_ID, "zowe.apiml.test"); + metadata.put(API_INFO + ".1." + API_INFO_VERSION, "1.0.0"); + metadata.put(API_INFO + ".1." + API_INFO_IS_DEFAULT, "true"); + + List info = eurekaMetadataParser.parseApiInfo(metadata); + + assertEquals(2, info.size()); + assertEquals("gatewayUrl", info.get(0).getGatewayUrl()); + assertEquals("zowe.apiml.test", info.get(0).getApiId()); + assertEquals("1.0.0", info.get(0).getVersion()); + assertTrue(info.get(0).isDefaultApi()); + assertEquals("gatewayUrl2", info.get(1).getGatewayUrl()); + assertEquals("swagger", info.get(1).getSwaggerUrl()); + assertEquals("doc", info.get(1).getDocumentationUrl()); + assertFalse(info.get(1).isDefaultApi()); + } + + @Test + void givenCodeSnippets_thenReturnApiInfoWithCodeSnippets() { + Map metadata = new HashMap<>(); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".1." + CODE_SNIPPET_ENDPOINT, "endpoint1"); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".1." + CODE_SNIPPET_CODE_BLOCK, "codeblock1"); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".1." + CODE_SNIPPET_LANGUAGE, "language1"); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".2." + CODE_SNIPPET_ENDPOINT, "endpoint2"); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".2." + CODE_SNIPPET_CODE_BLOCK, "codeblock2"); + metadata.put(API_INFO + ".1." + CODE_SNIPPET + ".2." + CODE_SNIPPET_LANGUAGE, "language2"); + metadata.put(API_INFO + ".2." + CODE_SNIPPET, "badvalue"); + + List info = eurekaMetadataParser.parseApiInfo(metadata); + + CodeSnippet expectedCodeSnippet1 = new CodeSnippet("endpoint1", "codeblock1", "language1"); + CodeSnippet expectedCodeSnippet2 = new CodeSnippet("endpoint2", "codeblock2", "language2"); + + assertEquals(1, info.size()); + assertEquals(expectedCodeSnippet1, info.get(0).getCodeSnippet().get(0)); + assertEquals(expectedCodeSnippet2, info.get(0).getCodeSnippet().get(1)); + } + + @Test + void givenBadField_thenDontReturnInstance() { + Map metadata = new HashMap<>(); + metadata.put(API_INFO + ".1." + API_INFO_API_ID, "zowe.apiml.test"); + metadata.put(API_INFO + ".2." + "badargument", "value"); + + List info = eurekaMetadataParser.parseApiInfo(metadata); + assertEquals(1, info.size()); + } } @Test @@ -71,11 +109,11 @@ void testParseRoutes() { RoutedServices expectedRoutes = new RoutedServices(); expectedRoutes.addRoutedService( - new RoutedService("api-v1", "api/v1", "/")); + new RoutedService("api-v1", "api/v1", "/")); expectedRoutes.addRoutedService( - new RoutedService("api-v2", "api/v2", "/test")); + new RoutedService("api-v2", "api/v2", "/test")); expectedRoutes.addRoutedService( - new RoutedService("api-v5", "api/v5", "/test")); + new RoutedService("api-v5", "api/v5", "/test")); assertEquals(expectedRoutes.toString(), routes.toString()); } @@ -96,9 +134,9 @@ void testParseToListRoute() { List actualRoutes = eurekaMetadataParser.parseToListRoute(metadata); List expectedListRoute = Arrays.asList( - new RoutedService("api-v1", "api/v1", "/"), - new RoutedService("api-v2", "api/v2", "/test"), - new RoutedService("api-v5", "api/v5", "/test") + new RoutedService("api-v1", "api/v1", "/"), + new RoutedService("api-v2", "api/v2", "/test"), + new RoutedService("api-v5", "api/v5", "/test") ); assertEquals(3, actualRoutes.size(), "List route size is different"); @@ -143,6 +181,38 @@ void generateFullMetadata() { assertEquals(documentationUrl, metaDocumentationUrl); } + @Test + void generateMetadataWithCodeSnippets() { + String endpoint1 = "/endpoint1"; + String codeBlock1 = "code1"; + String language1 = "java1"; + String endpoint2 = "/endpoint2"; + String codeBlock2 = "code2"; + String language2 = "java2"; + String metadataPrefix = API_INFO + ".api-v1."; + + ApiInfo apiInfo = new ApiInfo("zowe.apiml.test", "api/v1", "1.0.0", "https://service/api-doc", "https://www.zowe.org"); + apiInfo.addCodeSnippet(new CodeSnippet(endpoint1, codeBlock1, language1)); + apiInfo.addCodeSnippet(new CodeSnippet(endpoint2, codeBlock2, language2)); + Map metadata = EurekaMetadataParser.generateMetadata("test service", apiInfo); + + String codeSnippetEndpoint1 = metadata.get(metadataPrefix + CODE_SNIPPET + ".0." + CODE_SNIPPET_ENDPOINT); + String codeSnippetBlock1 = metadata.get(metadataPrefix + CODE_SNIPPET + ".0." + CODE_SNIPPET_CODE_BLOCK); + String codeSnippetLanguage1 = metadata.get(metadataPrefix + CODE_SNIPPET + ".0." + CODE_SNIPPET_LANGUAGE); + + assertEquals(codeSnippetEndpoint1, endpoint1); + assertEquals(codeSnippetBlock1, codeBlock1); + assertEquals(codeSnippetLanguage1, language1); + + String codeSnippetEndpoint2 = metadata.get(metadataPrefix + CODE_SNIPPET + ".1." + CODE_SNIPPET_ENDPOINT); + String codeSnippetBlock2 = metadata.get(metadataPrefix + CODE_SNIPPET + ".1." + CODE_SNIPPET_CODE_BLOCK); + String codeSnippetLanguage2 = metadata.get(metadataPrefix + CODE_SNIPPET + ".1." + CODE_SNIPPET_LANGUAGE); + + assertEquals(codeSnippetEndpoint2, endpoint2); + assertEquals(codeSnippetBlock2, codeBlock2); + assertEquals(codeSnippetLanguage2, language2); + } + @Test void generateMetadataWithNoGatewayUrl() { String serviceId = "test service"; diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 010211891d..21c73ac9e2 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -54,6 +54,13 @@ services: - apiId: zowe.apiml.discoverableclient gatewayUrl: api/v1 version: 1.0.0 + codeSnippet: + - endpoint: /pets + language: java + codeBlock: | + ``` + System.out.println("Pets code snippet"); + ``` - serviceId: zowejwt # unique lowercase ID of the service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) diff --git a/discoverable-client/src/main/resources/application.yml b/discoverable-client/src/main/resources/application.yml index 9e568dc2da..3ef7d55687 100644 --- a/discoverable-client/src/main/resources/application.yml +++ b/discoverable-client/src/main/resources/application.yml @@ -75,6 +75,19 @@ apiml: swaggerUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v3/api-docs/apiv1 documentationUrl: https://www.zowe.org defaultApi: true + codeSnippet: + - endpoint: /greeting + language: java + codeBlock: | + ``` + System.out.println("Greeting code snippet"); + ``` + - endpoint: /{yourName}/greeting + language: java + codeBlock: | + ``` + System.out.println("Your name greeting code snippet"); + ``` - apiId: zowe.apiml.discoverableclient.ws version: 1.0.0 gatewayUrl: graphql/v1 diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java index c294124c65..51db84b97f 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/staticdef/ServiceDefinitionProcessor.java @@ -351,7 +351,7 @@ private void setMetadataTile(Map metadata, CatalogUiTile tile) { metadata.put(CATALOG_DESCRIPTION, tile.getDescription()); } - private void setMetadataAppInfo(Map metadata, List appInfoList, String serviceId) { + private void setMetadataApiInfo(Map metadata, List appInfoList, String serviceId) { if (appInfoList == null) return; for (ApiInfo apiInfo : appInfoList) { @@ -392,7 +392,7 @@ private Map createMetadata(Service service, URL url, CatalogUiTi setMetadataRoutes(metadata, service.getRoutes(), url); setMetadataTile(metadata, tile); - setMetadataAppInfo(metadata, service.getApiInfo(), service.getServiceId()); + setMetadataApiInfo(metadata, service.getApiInfo(), service.getServiceId()); setMetadataAuthentication(metadata, service.getAuthentication()); setCustomMetadata(metadata, service.getCustomMetadata()); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java index 4bf6cd70c8..f20ab18164 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java @@ -130,6 +130,7 @@ private List getApiInfos(List appInst )) .documentationUrl(apiInfo.getDocumentationUrl()) .version(apiInfo.getVersion()) + .codeSnippet(apiInfo.getCodeSnippet()) .isDefaultApi(apiInfo.isDefaultApi()) .build()) .collect(Collectors.toList())); diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java index 0ec3d02700..14003ba599 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/discovery/ApiCatalogDiscoverableClientIntegrationTest.java @@ -11,6 +11,7 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import net.minidev.json.JSONArray; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; @@ -30,6 +31,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isEmptyString; import static org.junit.jupiter.api.Assertions.*; import static org.zowe.apiml.util.requests.Endpoints.*; @@ -90,10 +92,7 @@ void givenUrlForContainer() throws IOException { String containerJsonResponse = EntityUtils.toString(response.getEntity()); DocumentContext containerJsonContext = JsonPath.parse(containerJsonResponse); - // Validate container - assertEquals("cademoapps", containerJsonContext.read("$[0].id")); - assertEquals("Sample API Mediation Layer Applications", containerJsonContext.read("$[0].title")); - assertEquals("UP", containerJsonContext.read("$[0].status")); + validateContainer(containerJsonContext); // Get Discoverable Client swagger String dcJsonResponse = containerJsonContext.read("$[0].services[0]").toString(); @@ -104,6 +103,38 @@ void givenUrlForContainer() throws IOException { } } + @Test + void givenApis_whenGetContainer_thenApisReturned() throws IOException { + HttpResponse response = getResponse(DISCOVERABLE_CLIENT_CONTAINER_ENDPOINT, HttpStatus.SC_OK); + String containerJsonResponse = EntityUtils.toString(response.getEntity()); + DocumentContext containerJsonContext = JsonPath.parse(containerJsonResponse); + + validateContainer(containerJsonContext); + + LinkedHashMap> apis = containerJsonContext.read("$[0].services[0].apis"); + + assertNotNull(apis.get("default")); + assertNotNull(apis.get("zowe.apiml.discoverableclient.rest v2.0.0")); + assertNotNull(apis.get("zowe.apiml.discoverableclient.rest v1.0.0")); + assertNotNull(apis.get("zowe.apiml.discoverableclient.ws v1.0.0")); + + LinkedHashMap defaultApi = apis.get("default"); + assertEquals("zowe.apiml.discoverableclient.rest", defaultApi.get("apiId")); + assertEquals("api/v1", defaultApi.get("gatewayUrl")); + assertEquals("1.0.0", defaultApi.get("version")); + assertThat((String) defaultApi.get("swaggerUrl"), endsWith("/discoverableclient/v3/api-docs/apiv1")); + assertEquals("https://www.zowe.org", defaultApi.get("documentationUrl")); + assertEquals(true, defaultApi.get("defaultApi")); + + JSONArray codeSnippets = (JSONArray) defaultApi.get("codeSnippet"); + assertEquals(2, codeSnippets.size()); + LinkedHashMap codeSnippet = (LinkedHashMap) codeSnippets.get(0); + assertEquals("/greeting", codeSnippet.get("endpoint")); + assertNotNull(codeSnippet.get("codeBlock")); + assertThat(codeSnippet.get("codeBlock"), not(isEmptyString())); + assertEquals("java", codeSnippet.get("language")); + } + @Nested class WhenGettingDifferenceBetweenVersions { @Nested @@ -152,6 +183,12 @@ private HttpResponse getResponse(String endpoint, int returnCode) throws IOExcep return response; } + private void validateContainer(DocumentContext containerJsonContext) { + assertEquals("cademoapps", containerJsonContext.read("$[0].id")); + assertEquals("Sample API Mediation Layer Applications", containerJsonContext.read("$[0].title")); + assertEquals("UP", containerJsonContext.read("$[0].status")); + } + private void validateDiscoverableClientApiV1(String jsonResponse, DocumentContext jsonContext) throws IOException { String apiCatalogSwagger = "\n**************************\n" + "Integration Test: Discoverable Client Swagger" +