Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ApisJsonInterceptor #1189

Merged
merged 18 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/* Copyright 2024 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */
package com.predic8.membrane.core.interceptor;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.predic8.membrane.annot.MCAttribute;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.core.Router;
import com.predic8.membrane.core.config.Path;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.Response.ResponseBuilder;
import com.predic8.membrane.core.openapi.serviceproxy.*;
import com.predic8.membrane.core.openapi.serviceproxy.APIProxy.ApiDescription;
import com.predic8.membrane.core.rules.Rule;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static java.lang.String.valueOf;
import static java.text.DateFormat.getDateTimeInstance;
import static java.util.Optional.ofNullable;

@MCElement(name = "APIsJSON")
public class ApisJsonInterceptor extends AbstractInterceptor {

private static final Logger log = LoggerFactory.getLogger(ApisJsonInterceptor.class);

private static final ObjectMapper om = new ObjectMapper();
private static final String YYYY_MM_DD = "yyyy-MM-dd";
private static final String SPECIFICATION_VERSION = "0.18";
private byte[] apisJson;

private String rootDomain = "membrane";
private String collectionId = "apis";
private String collectionName = "APIs";
private String description = "APIs.json Document";
private String apisJsonUrl;
private Date created = new Date();
private Date modified = new Date();

@Override
public Outcome handleRequest(Exchange exc) throws Exception {
if (apisJson == null)
initJson(router, exc);
exc.setResponse(new ResponseBuilder().body(apisJson).contentType(APPLICATION_JSON).build());
return RETURN;
}

public void initJson(Router router, Exchange exc) throws JsonProcessingException {
if (apisJson != null) {
return;
}
if (apisJsonUrl == null) {
apisJsonUrl = getProtocol(exc.getRule()) + exc.getRequest().getHeader().getHost() + exc.getRequest().getUri();
}
ObjectNode apis = om.createObjectNode();
apis.put("aid", rootDomain + ":" + collectionId);
apis.put("name", collectionName);
apis.put("description", description);
apis.put("url", apisJsonUrl);
apis.put("created", new SimpleDateFormat(YYYY_MM_DD).format(created));
apis.put("modified", new SimpleDateFormat(YYYY_MM_DD).format(modified));
apis.put("specificationVersion", SPECIFICATION_VERSION);
apis.putArray("apis").addAll(
(router.getRuleManager().getRules().stream()
.filter(APIProxy.class::isInstance)
.<JsonNode>mapMulti((rule, sink) -> {
var r = ((APIProxy) rule);
if (r.getApiRecords().isEmpty())
sink.accept(jsonNodeFromApiProxy(r, null, null));
else
r.getApiRecords().forEach((id, rec) -> sink.accept(jsonNodeFromApiProxy(r, id, rec)));
}).toList())
);
apisJson = om.writeValueAsBytes(apis);
}

JsonNode jsonNodeFromApiProxy(APIProxy api, String recordId, OpenAPIRecord apiRecord) {
ObjectNode apiJson = om.createObjectNode();
apiJson.put("aid", customIdOrBuildDefault(api, recordId));
apiJson.put("name", (apiRecord != null) ? apiRecord.getApi().getInfo().getTitle() : api.getName());
apiJson.put("description", (apiRecord != null && api.getDescription() == null
&& apiRecord.getApi().getInfo().getDescription() != null)
? apiRecord.getApi().getInfo().getDescription()
: ofNullable(api.getDescription()).map(ApiDescription::getContent).orElse("API"));
apiJson.put("humanUrl", getProtocol(api) + getHost(api) + ((apiRecord != null) ? "/api-docs/ui/" + recordId : "/api-docs"));
apiJson.put("baseUrl", getProtocol(api) + getHost(api) + ofNullable(api.getPath()).map(Path::getValue).orElse("/"));
if (apiRecord != null)
apiJson.put("version", apiRecord.getApi().getInfo().getVersion());
return apiJson;
}

private static String getHost(APIProxy api) {
String hostname = (Objects.equals(api.getKey().getHost(), "*")) ? "localhost" : api.getKey().getHost();

return hostname + ((api.getPort() == 80 || api.getPort() == 443) ? "" : ":" + api.getPort());
}

private String customIdOrBuildDefault(APIProxy api, String recordId) {
if (api.getId() != null)
return api.getId();
return (recordId != null) ? rootDomain + ":" + recordId : buildDefaultAPIProxyId(api);
}

private String buildDefaultAPIProxyId(APIProxy api) {
return rootDomain + ":" + ((APIProxyKey) api.getKey()).getKeyId();
}

private static @NotNull String getProtocol(Rule rule) {
return (rule.getSslInboundContext() != null ? "https" : "http") + "://";
}

@MCAttribute
public void setRootDomain(String rootDomain) {
this.rootDomain = rootDomain;
}

@MCAttribute
public void setCollectionId(String collectionId) {
this.collectionId = collectionId;
}

@MCAttribute(attributeName = "name")
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}

@MCAttribute
public void setDescription(String description) {
this.description = description;
}

@MCAttribute(attributeName = "url")
public void setApisJsonUrl(String apisJsonUrl) {
this.apisJsonUrl = apisJsonUrl;
}

@MCAttribute
public void setCreated(String created) throws ParseException {
this.created = new SimpleDateFormat(YYYY_MM_DD).parse(created);
}

@MCAttribute
public void setModified(String modified) throws ParseException {
this.modified = new SimpleDateFormat(YYYY_MM_DD).parse(modified);
}

@Override
public String getDisplayName() {
return "APIs Json";
}

@Override
public String getShortDescription() {
return "Displays all deployed API Proxies in APIs.json format";
}

@Override
public String getLongDescription() {
return "WIP"; // TODO Add long description
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class APIProxy extends ServiceProxy {
public static final String VALIDATION_DETAILS = "details";

private String test;
private String id;
private ApiDescription description;

protected Map<String,OpenAPIRecord> apiRecords = new LinkedHashMap<>();

Expand Down Expand Up @@ -158,6 +160,10 @@ public ValidationStatisticsCollector getValidationStatisticCollector() {
return statisticCollector;
}

public Map<String, OpenAPIRecord> getApiRecords() {
return apiRecords;
}

public Map<String, OpenAPIRecord> getBasePaths() {
return basePaths;
}
Expand All @@ -171,4 +177,35 @@ public void setTest(String test) {
this.test = test;
}

public String getId() {
return id;
}

public ApiDescription getDescription() {
return description;
}

@MCAttribute
public void setId(String id) {
this.id = id;
}

@MCChildElement
public void setDescription(ApiDescription description) {
this.description = description;
}

@MCElement(name = "description", topLevel = false, mixed = true)
public static class ApiDescription {
private String content;

@MCTextContent
public void setContent(String content) {
this.content = content;
}

public String getContent() {
return content;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

import java.util.*;

import static java.util.Optional.ofNullable;

public class APIProxyKey extends ServiceProxyKey {

private static final Logger log = LoggerFactory.getLogger(APIProxyKey.class.getName());
Expand Down Expand Up @@ -91,6 +93,17 @@ void addBasePaths(ArrayList<String> paths) {
basePaths.addAll(paths);
}

public String getKeyId() {
return (
getMethod() + "-"
+ ofNullable(getIp()).orElse("0.0.0.0") + "-"
+ getHost()
+ getPort()
+ getPath() + "-"
+ (testExpr == null ? "true" : testExpr.getExpressionString())
);
}

@Override
public boolean equals(Object obj) {
if (!super.equals(obj))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* Copyright 2024 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */
package com.predic8.membrane.core.interceptor;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.predic8.membrane.core.Router;
import com.predic8.membrane.core.RuleManager;
import com.predic8.membrane.core.config.Path;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.Request.Builder;
import com.predic8.membrane.core.openapi.serviceproxy.APIProxy;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.text.ParseException;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class ApisJsonInterceptorTest {
private static final String RESPONSE_JSON = """
{"aid":"example.com:1234","name":"API Collection","description":"API Collection Description","url":"http://example.com/apis.json","created":"2024-07-15","modified":"2024-07-15","specificationVersion":"0.18","apis": [{"aid":"example.com:/","name":":80","description":"N/A (Available only internally in APIProxy)","humanUrl":"WIP","baseUrl":"/","image":"N/A (Available only internally in APIProxy)","version":"N/A (Available only internally in APIProxy)"}]}""";
private static ApisJsonInterceptor interceptor;

@BeforeAll
static void setUp() throws ParseException {
interceptor = new ApisJsonInterceptor() {{
setRootDomain("example.com");
setCollectionId("1234");
setCollectionName("API Collection");
setDescription("API Collection Description");
setApisJsonUrl("http://example.com/apis.json");
setCreated("2024-07-15");
setModified("2024-07-15");
}};
}

@Test
void responseTest() throws Exception {
Exchange exc = new Builder().buildExchange();
Router r = mock(Router.class);
RuleManager rm = mock(RuleManager.class);
when(r.getRuleManager()).thenReturn(rm);
when(rm.getRules()).thenReturn(List.of(new APIProxy()));
interceptor.init(r);
interceptor.handleRequest(exc);
assertEquals(RESPONSE_JSON, exc.getResponse().getBodyAsStringDecoded());
}

@Test
void jsonNodeFromApiProxyTest() {
APIProxy apiProxy = new APIProxy() {{
setPath(new Path(false, "/baz"));
setName("Demo API");
}};

JsonNode apiNode = interceptor.jsonNodeFromApiProxy(apiProxy);

assertEquals("example.com" + ":/baz", apiNode.get("aid").asText());
assertEquals("Demo API", apiNode.get("name").asText());
assertEquals("N/A (Available only internally in APIProxy)", apiNode.get("description").asText());
assertEquals("WIP", apiNode.get("humanUrl").asText());
assertEquals("/baz", apiNode.get("baseUrl").asText());
assertEquals("N/A (Available only internally in APIProxy)", apiNode.get("image").asText());
assertEquals("N/A (Available only internally in APIProxy)", apiNode.get("version").asText());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@
limitations under the License. */
package com.predic8.membrane.core.rules;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

import com.predic8.membrane.core.http.Request;
import org.junit.jupiter.api.Test;

import com.predic8.membrane.core.http.Request;
import static org.junit.jupiter.api.Assertions.*;


public class ServiceProxyKeyTest {
Expand Down
8 changes: 0 additions & 8 deletions distribution/conf/proxies.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,13 @@

<router>

<!-- <serviceProxy port="2000">-->
<!-- <path>/shop/v2</path>-->
<!-- <target host="api.predic8.de">-->
<!-- <ssl/>-->
<!-- </target>-->
<!-- </serviceProxy>-->

<api port="2000">
<path>/shop/v2</path>
<target host="api.predic8.de">
<ssl/>
</target>
</api>


</router>

</spring:beans>
Loading