Spring 云网关 - 修改全局 Post 过滤器中的响应 body
Spring Cloud Gateway - modify response body in global Post filter
在过去的 2 天里,我尝试了所有可能的方法来在请求到达客户端之前修改请求的响应 body,但似乎对我没有任何作用。到目前为止,我已经尝试了提到的实现 here, , here, here, here 和其他一些我现在找不到的实现,但没有任何效果。我将过滤器定义为 pre、post、global、gateway 或 route-specific 都没关系——实际的响应修改似乎对我不起作用。
我的情况是这样的:
我有一个 YAML-configured API 网关 运行ning 并配置了其中一条路由以在后台通向 ADF 服务。这个 ADF 应用程序的问题是它 returns 对客户端的响应采用后端自动生成的 HTML 模板的形式。在此模板中,一些 URL 是硬编码的,指向应用程序本身的地址。为了证明在这种情况下使用 API 网关是合理的,我想用 API 网关替换那些 ADF URL。
为简单起见,假设我的 ADF 服务的 IP 地址是 1.2.3.4:1234
,我的 API 网关的 IP 地址是 localhost:8080
。当我在网关中访问 ADF 路由时,响应包含一些 auto-generated javascript 插入,例如:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://1.2.3.4:1234/entry/dynamic/index.jspx");
如您所见,它包含一个硬编码的 URL。我想访问响应 body 并找到所有那些硬编码的 URL 并将它们替换为网关 URL,因此上面的示例变为:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://localhost:8080/entry/dynamic/index.jspx");
要做到这一点,对我来说,拥有一个全局 POST 过滤器似乎是明智的,它仅在请求与我的 ADF 应用程序的路由匹配时才启动,所以这就是我决定做的事情。
到目前为止,这是我的 post 过滤器:
@Bean
public GlobalFilter globalADFUrlReplacementFilter() {
return (exchange, chain) -> chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (requestIsTowardsADF(request)) {
logger.info("EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT");
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
@Override
@SuppressWarnings("unchecked")
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
logger.info("OVERRIDING writeWith METHOD TO MODIFY THE BODY");
Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(buffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(buffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
bodyStr = bodyStr.replace(ADF_URL, API_GATEWAY_URL);
getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
return bufferFactory().wrap(bodyStr.getBytes());
}));
}
};
logger.info("ADF URL REPLACEMENT FILTER DONE");
return chain.filter(serverWebExchange.mutate().request(request).response(responseDecorator).build());
}
return serverWebExchange;
})
.then();
}
和配置:
spring:
cloud:
gateway:
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
您可以看到我正在使用 org.slf4j.Logger
object 在控制台中记录消息。当我 运行 我的 API 网关并访问 ADF 路由时,我可以看到以下内容:
EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT
ADF URL REPLACEMENT FILTER DONE
当我检查从 API 网关返回的响应时,我可以看到响应 body 仍然相同并且 ADF URL 没有被替换根本。我尝试调试该应用程序,一旦它到达 ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
,它就会跳过那些大括号内的整个匿名 class 实现。证明是控制台中没有 OVERRIDING writeWith METHOD TO MODIFY THE BODY
日志 - 它从未执行过!
似乎由于某种原因实际的 body 修改没有执行,我不明白为什么。如以上链接所述,我尝试了此过滤器的几种不同实现方式,但均无效。
有人可以与我分享一个有效的 POST 过滤器来修改响应 body,或者指出我的解决方案中的缺陷吗?
提前致谢!
试试 built-in ModifyResponseBody Filter with Java DSL. If you still need more advanced response processing, your next option is to extend the ModifyResponseBodyGatewayFilterFactory class.
(更新2022-05-08)
例如,使用委托设计模式(将 built-in ModifyResponseBodyFilter 包装在一个新的自定义过滤器中,采用一个自定义参数):
package test;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.*;
@Component
public class MyFilterFactory extends AbstractGatewayFilterFactory<MyFilterFactory.Config>
{
public static class Config
{
private String param1;
// Add other parameters if necessary
public Config() {}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
// Add getters and setters for other parameters if any
}
@Override
public List<String> shortcutFieldOrder()
{
return Arrays.asList("param1" /*, other parameters */ );
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public MyFilterFactory()
{
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
@Override
public GatewayFilter apply(Config config)
{
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setNewContentType(MediaType.TEXT_HTML_VALUE);
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> {
final String output;
/*
Do whatever transformation of bodyAsString (response body as String) and assign the result to output...
*/
return Mono.just(output);
});
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
感谢分享这个示例过滤器 cdan。我使用它作为模板为我的问题提供了最直接的解决方案。外观如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
final Logger logger = LoggerFactory.getLogger(TestFilter2.class);
public static class Config {
private String param1;
public Config() {
}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("param1");
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public TestFilter2() {
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
@Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
我已将此过滤器添加到我的路由定义中,如下所示:
spring:
cloud:
gateway:
httpclient:
wiretap: true
httpserver:
wiretap: true
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
filters:
- TestFilter2
我只是想修改响应主体并将其中的 ADF URL 替换为 AG URL,但每当我尝试访问 ADF 路由时,我都会遇到以下异常:
2022-05-08 17:35:19.492 ERROR 87216 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [284b180d-1] 500 Server Error for HTTP GET "/entry/dynamic/index.jspx"
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String
at org.springframework.web.reactive.function.BodyExtractors.lambda$readWithMessageReaders(BodyExtractors.java:201) ~[spring-webflux-5.3.18.jar:5.3.18]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ? Body from UNKNOWN [DefaultClientResponse]
*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ? HTTP GET "/entry/dynamic/index.jspx" [ExceptionHandlingWebHandler]
我在网上搜索了一段时间,但无法找到任何明确的答案来说明为什么当我尝试使用本应包含作为字符串的响应主体。调试整个过滤器也没有用,因为异常似乎在我到达路由后立即抛出,我什至无法进入 class 的主体。我是否漏掉了一些明显的东西?
更新 (09.05.2022):
在进一步研究之后,我通过删除配置中不必要的参数稍微重构了过滤器结构,并将依赖项自动装配到 ModifyResponseBodyGatewayFilterFactory
,现在看来过滤器工作正常并进行了我需要它做的替换.我将对其进行更长时间的测试,以确保它确实按预期工作,如果确实如此,我会将其标记为解决方案。感谢您的所有输入 cdan!
这是整个过滤器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
@Autowired
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public static class Config {
public Config() {
}
}
public TestFilter2(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) {
super(Config.class);
this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory;
}
@Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
在过去的 2 天里,我尝试了所有可能的方法来在请求到达客户端之前修改请求的响应 body,但似乎对我没有任何作用。到目前为止,我已经尝试了提到的实现 here,
我的情况是这样的: 我有一个 YAML-configured API 网关 运行ning 并配置了其中一条路由以在后台通向 ADF 服务。这个 ADF 应用程序的问题是它 returns 对客户端的响应采用后端自动生成的 HTML 模板的形式。在此模板中,一些 URL 是硬编码的,指向应用程序本身的地址。为了证明在这种情况下使用 API 网关是合理的,我想用 API 网关替换那些 ADF URL。
为简单起见,假设我的 ADF 服务的 IP 地址是 1.2.3.4:1234
,我的 API 网关的 IP 地址是 localhost:8080
。当我在网关中访问 ADF 路由时,响应包含一些 auto-generated javascript 插入,例如:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://1.2.3.4:1234/entry/dynamic/index.jspx");
如您所见,它包含一个硬编码的 URL。我想访问响应 body 并找到所有那些硬编码的 URL 并将它们替换为网关 URL,因此上面的示例变为:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://localhost:8080/entry/dynamic/index.jspx");
要做到这一点,对我来说,拥有一个全局 POST 过滤器似乎是明智的,它仅在请求与我的 ADF 应用程序的路由匹配时才启动,所以这就是我决定做的事情。
到目前为止,这是我的 post 过滤器:
@Bean
public GlobalFilter globalADFUrlReplacementFilter() {
return (exchange, chain) -> chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (requestIsTowardsADF(request)) {
logger.info("EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT");
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
@Override
@SuppressWarnings("unchecked")
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
logger.info("OVERRIDING writeWith METHOD TO MODIFY THE BODY");
Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(buffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(buffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
bodyStr = bodyStr.replace(ADF_URL, API_GATEWAY_URL);
getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
return bufferFactory().wrap(bodyStr.getBytes());
}));
}
};
logger.info("ADF URL REPLACEMENT FILTER DONE");
return chain.filter(serverWebExchange.mutate().request(request).response(responseDecorator).build());
}
return serverWebExchange;
})
.then();
}
和配置:
spring:
cloud:
gateway:
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
您可以看到我正在使用 org.slf4j.Logger
object 在控制台中记录消息。当我 运行 我的 API 网关并访问 ADF 路由时,我可以看到以下内容:
EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT
ADF URL REPLACEMENT FILTER DONE
当我检查从 API 网关返回的响应时,我可以看到响应 body 仍然相同并且 ADF URL 没有被替换根本。我尝试调试该应用程序,一旦它到达 ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
,它就会跳过那些大括号内的整个匿名 class 实现。证明是控制台中没有 OVERRIDING writeWith METHOD TO MODIFY THE BODY
日志 - 它从未执行过!
似乎由于某种原因实际的 body 修改没有执行,我不明白为什么。如以上链接所述,我尝试了此过滤器的几种不同实现方式,但均无效。
有人可以与我分享一个有效的 POST 过滤器来修改响应 body,或者指出我的解决方案中的缺陷吗?
提前致谢!
试试 built-in ModifyResponseBody Filter with Java DSL. If you still need more advanced response processing, your next option is to extend the ModifyResponseBodyGatewayFilterFactory class.
(更新2022-05-08) 例如,使用委托设计模式(将 built-in ModifyResponseBodyFilter 包装在一个新的自定义过滤器中,采用一个自定义参数):
package test;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.*;
@Component
public class MyFilterFactory extends AbstractGatewayFilterFactory<MyFilterFactory.Config>
{
public static class Config
{
private String param1;
// Add other parameters if necessary
public Config() {}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
// Add getters and setters for other parameters if any
}
@Override
public List<String> shortcutFieldOrder()
{
return Arrays.asList("param1" /*, other parameters */ );
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public MyFilterFactory()
{
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
@Override
public GatewayFilter apply(Config config)
{
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setNewContentType(MediaType.TEXT_HTML_VALUE);
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> {
final String output;
/*
Do whatever transformation of bodyAsString (response body as String) and assign the result to output...
*/
return Mono.just(output);
});
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
感谢分享这个示例过滤器 cdan。我使用它作为模板为我的问题提供了最直接的解决方案。外观如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
final Logger logger = LoggerFactory.getLogger(TestFilter2.class);
public static class Config {
private String param1;
public Config() {
}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("param1");
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public TestFilter2() {
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
@Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
我已将此过滤器添加到我的路由定义中,如下所示:
spring:
cloud:
gateway:
httpclient:
wiretap: true
httpserver:
wiretap: true
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
filters:
- TestFilter2
我只是想修改响应主体并将其中的 ADF URL 替换为 AG URL,但每当我尝试访问 ADF 路由时,我都会遇到以下异常:
2022-05-08 17:35:19.492 ERROR 87216 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [284b180d-1] 500 Server Error for HTTP GET "/entry/dynamic/index.jspx"
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String
at org.springframework.web.reactive.function.BodyExtractors.lambda$readWithMessageReaders(BodyExtractors.java:201) ~[spring-webflux-5.3.18.jar:5.3.18]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ? Body from UNKNOWN [DefaultClientResponse]
*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ? HTTP GET "/entry/dynamic/index.jspx" [ExceptionHandlingWebHandler]
我在网上搜索了一段时间,但无法找到任何明确的答案来说明为什么当我尝试使用本应包含作为字符串的响应主体。调试整个过滤器也没有用,因为异常似乎在我到达路由后立即抛出,我什至无法进入 class 的主体。我是否漏掉了一些明显的东西?
更新 (09.05.2022):
在进一步研究之后,我通过删除配置中不必要的参数稍微重构了过滤器结构,并将依赖项自动装配到 ModifyResponseBodyGatewayFilterFactory
,现在看来过滤器工作正常并进行了我需要它做的替换.我将对其进行更长时间的测试,以确保它确实按预期工作,如果确实如此,我会将其标记为解决方案。感谢您的所有输入 cdan!
这是整个过滤器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
@Autowired
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public static class Config {
public Config() {
}
}
public TestFilter2(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) {
super(Config.class);
this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory;
}
@Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}