Skip to content

Commit

Permalink
Add JsonPath replacer (#1261)
Browse files Browse the repository at this point in the history
* Implement JSON Path replacement interceptor and associated tests

Add `ReplaceInterceptor` class to handle JSON Path replacements in requests. Introduce necessary dependencies for JSON Path processing. Enhance unit tests for the interceptor, ensuring functionality with various JSON structures and scenarios.

* Refactor ReplaceInterceptorTest to improve readability and maintainability

- Rename instance variable to `replaceInterceptor` for clarity
- Utilize `ObjectMapper` for JSON parsing in assertions
- Format JSON strings for better readability in test cases

* Add JavaDoc comments to setJsonPath and setReplacement methods in ReplaceInterceptor class

* Update JSONPath dependency to version 2.9.0 and refactor ReplaceInterceptor class

- Upgrade `json-path` dependency in `pom.xml`.
- Change `ReplaceInterceptor` class annotation from `@MCElement(name="jsonPathReplacer")` to `@MCElement(name="replace")`.
- Enhance `replaceWithJsonPath` method with logging and use a custom JSON provider.
- Add example scripts for running the service proxy in both Bash and Batch formats.
- Include a README with a sample curl command for testing the replacement functionality.

* added test

* requested changes

* requested changes

* requested changes

* requested changes

* Update README and proxies.xml to replace target API configuration with return directive
  • Loading branch information
christiangoerdes authored Sep 5, 2024
1 parent 1bd5a25 commit 46d078d
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 0 deletions.
5 changes: 5 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@
<artifactId>oauth2-openid</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.predic8.membrane.core.interceptor.json;

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.predic8.membrane.annot.MCAttribute;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.Message;
import com.predic8.membrane.core.interceptor.AbstractInterceptor;
import com.predic8.membrane.core.interceptor.Outcome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE;

@SuppressWarnings("unused")
@MCElement(name="replace")
public class ReplaceInterceptor extends AbstractInterceptor {

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

private String jsonPath;

private String with;

@Override
public Outcome handleRequest(Exchange exc) throws Exception {
return handleInternal(exc.getRequestContentType(), exc.getRequest());
}

@Override
public Outcome handleResponse(Exchange exc) throws Exception {
return handleInternal(exc.getResponseContentType(), exc.getResponse());
}

private Outcome handleInternal(String contentType, Message msg) {
if(contentType.equals(APPLICATION_JSON)) {
msg.setBodyContent(replaceWithJsonPath(msg, jsonPath, with).getBytes());
}
return CONTINUE;
}

String replaceWithJsonPath(Message msg, String jsonPath, String replacement) {
Object document = Configuration.defaultConfiguration().jsonProvider().parse(msg.getBodyAsStringDecoded());
document = JsonPath.parse(document).set(jsonPath, replacement).json();
return Configuration.defaultConfiguration().jsonProvider().toJson(document);
}

/**
* Sets the JSONPath expression to identify the target node in the JSON structure.
*
* @param jsonPath the JSONPath expression (e.g., "$.person.name").
*/
@MCAttribute
public void setJsonPath(String jsonPath) {
this.jsonPath = jsonPath;
}

/**
* Sets the replacement value for the node specified by the JSONPath.
*
* @param with the new value to replace the existing one.
*/
@MCAttribute
public void setWith(String with) {
this.with = with;
}

public String getJsonPath() {return jsonPath;}

public String getWith() {return with;}


}
2 changes: 2 additions & 0 deletions core/src/test/java/com/predic8/membrane/core/UnitTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.predic8.membrane.core.interceptor.javascript.JavascriptInterceptor;
import com.predic8.membrane.core.interceptor.json.JsonPointerExtractorInterceptorTest;
import com.predic8.membrane.core.interceptor.json.JsonProtectionInterceptorTest;
import com.predic8.membrane.core.interceptor.json.ReplaceInterceptorTest;
import com.predic8.membrane.core.interceptor.log.AccessLogInterceptorTest;
import com.predic8.membrane.core.interceptor.misc.ReturnInterceptorTest;
import com.predic8.membrane.core.interceptor.misc.SetHeaderInterceptor;
Expand Down Expand Up @@ -160,6 +161,7 @@
CollectionsUtilTest.class,
ConditionalInterceptorGroovyTest.class,
ConditionalInterceptorSpELTest.class,
ReplaceInterceptorTest.class,
ApiKeysInterceptorTest.class,
ApiKeyFileStoreTest.class,
ApiKeyHeaderExtractorTest.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package com.predic8.membrane.core.interceptor.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.Message;
import com.predic8.membrane.core.http.Request;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.stream.Stream;

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

public class ReplaceInterceptorTest {

private ReplaceInterceptor replaceInterceptor;
private Message msg;
private static final ObjectMapper objectMapper = new ObjectMapper();

@BeforeEach
void setUp() throws URISyntaxException {
replaceInterceptor = new ReplaceInterceptor();
msg = Request.get("/foo").buildExchange().getRequest();
}

@ParameterizedTest
@MethodSource("jsonReplacementProvider")
void testReplaceWithJsonPath(String originalJson, String jsonPath, String replacement, String expectedJson) throws IOException {
msg.setBodyContent(originalJson.getBytes());
assertEquals(
objectMapper.readTree(expectedJson),
objectMapper.readTree(replaceInterceptor.replaceWithJsonPath(
msg,
jsonPath,
replacement)
)
);
}

private static Stream<Arguments> jsonReplacementProvider() {
return Stream.of(
Arguments.of(
"""
{
"name": "John",
"age": 30
}
""",
"$.name",
"Jane",
"""
{
"name": "Jane",
"age": 30
}
"""
),
Arguments.of(
"""
{
"person": {
"name": "John",
"age": 30
}
}
""",
"$.person.name",
"Jane",
"""
{
"person": {
"name": "Jane",
"age": 30
}
}
"""
),
Arguments.of(
"""
{
"people": [
{
"name": "John"
},
{
"name": "Doe"
}
]
}
""",
"$.people[0].name",
"Jane",
"""
{
"people": [
{
"name": "Jane"
},
{
"name": "Doe"
}
]
}
"""
),
Arguments.of(
"""
{
"family": {
"parents": [
{
"name": "John"
},
{
"name": "Doe"
}
]
}
}
""",
"$.family.parents[1].name",
"Jane",
"""
{
"family": {
"parents": [
{
"name": "John"
},
{
"name": "Jane"
}
]
}
}
"""
),
Arguments.of(
"""
{
"employees": [
{
"name": "John",
"role": "Manager"
},
{
"name": "Doe",
"role": "Developer"
}
]
}
""",
"$.employees[*].name",
"Jane",
"""
{
"employees": [
{
"name": "Jane",
"role": "Manager"
},
{
"name": "Jane",
"role": "Developer"
}
]
}
"""
),
Arguments.of(
"""
{
"company": {
"employees": [
{
"name": "John",
"department": {
"name": "HR"
}
},
{
"name": "Doe",
"department": {
"name": "IT"
}
}
]
}
}
""",
"$.company.employees[*].department.name",
"Operations",
"""
{
"company": {
"employees": [
{
"name": "John",
"department": {
"name": "Operations"
}
},
{
"name": "Doe",
"department": {
"name": "Operations"
}
}
]
}
}
"""
)
);
}


}
27 changes: 27 additions & 0 deletions distribution/examples/message-transformation/replace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Replace Plugin

The `Replace`plugin allows you to modify values in your JSON by using a `jsonPath` expression to target specific fields for replacement.
## Running the Example

1. Run `service-proxy.bat` or `service-proxy.sh`
2. Send a request using `curl`:

```shell
curl localhost:2000 \
-H "Content-Type: application/json" \
-d '{"user": {"name": "Alice", "age": 22}}'
```
and take a look at the output:
```json
{"user":{"name":"Bob","age":22}}
```

## Configuration

This configuration sets up an API that replaces the value of the `name` field under the `shop object` in the JSON body with "foo", before forwarding the request to a target service running on localhost at port 3000.
```xml
<api port="2000">
<replace jsonPath="$.user.name" with="Bob" />
<return />
</api>
```
16 changes: 16 additions & 0 deletions distribution/examples/message-transformation/replace/proxies.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<spring:beans xmlns:spring="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://membrane-soa.org/proxies/1/"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://membrane-soa.org/proxies/1/ http://membrane-soa.org/schemas/proxies-1.xsd">

<router>

<api port="2000">
<replace jsonPath="$.user.name" with="Bob" />
<return />
</api>

</router>

</spring:beans>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@echo off
if not "%MEMBRANE_HOME%" == "" goto homeSet
set "MEMBRANE_HOME=%cd%\..\..\.."
echo "%MEMBRANE_HOME%"
if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk

:homeSet
if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk
echo Please set the MEMBRANE_HOME environment variable to point to
echo the directory where you have extracted the Membrane software.
exit

:homeOk
set "CLASSPATH=%MEMBRANE_HOME%"
set "CLASSPATH=%MEMBRANE_HOME%/conf"
set "CLASSPATH=%CLASSPATH%;%MEMBRANE_HOME%/starter.jar"
echo Membrane Router running...
java -classpath "%CLASSPATH%" com.predic8.membrane.core.Starter -c proxies.xml
Loading

0 comments on commit 46d078d

Please sign in to comment.