PR #12441 - Allow HttpFields.asMap to modify the underlying HttpFields

Signed-off-by: Lachlan Roberts <lachlan.p.roberts@gmail.com>
This commit is contained in:
Lachlan Roberts 2024-10-31 17:03:05 +11:00
parent ec94ca551e
commit 7757941aad
No known key found for this signature in database
GPG Key ID: 5663FB7A8FF7E348
22 changed files with 436 additions and 73 deletions

View File

@ -928,6 +928,10 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
return size;
}
/**
* @param fields the {@link HttpFields} to convert to a {@link Map}.
* @return an unmodifiable {@link Map} representing the contents of the {@link HttpFields}.
*/
static Map<String, List<String>> asMap(HttpFields fields)
{
Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
@ -936,10 +940,20 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
if (!headers.containsKey(f.getName()))
{
HttpHeader header = f.getHeader();
headers.put(f.getName(), header == null ? fields.getValuesList(f.getName()) : fields.getValuesList(header));
List<String> values = header == null ? fields.getValuesList(f.getName()) : fields.getValuesList(header);
headers.put(f.getName(), Collections.unmodifiableList(values));
}
}
return headers;
return Collections.unmodifiableMap(headers);
}
/**
* @param fields the {@link HttpFields} to convert to a {@link Map}.
* @return a {@link Map} where changes to the contents will be reflected in the supplied {@link HttpFields}.
*/
static Map<String, List<String>> asMap(HttpFields.Mutable fields)
{
return new HttpFieldsMap(fields);
}
/**

View File

@ -0,0 +1,151 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jetty.util.StringUtil;
class HttpFieldsMap extends AbstractMap<String, List<String>>
{
private final HttpFields.Mutable httpFields;
public HttpFieldsMap(HttpFields.Mutable httpFields)
{
this.httpFields = httpFields;
}
@Override
public List<String> get(Object key)
{
if (key instanceof String s)
return httpFields.getValuesList(s);
return null;
}
@Override
public List<String> put(String key, List<String> value)
{
List<String> oldValue = get(key);
httpFields.put(key, value);
return oldValue;
}
@Override
public List<String> remove(Object key)
{
if (key instanceof String s)
{
List<String> oldValue = get(s);
httpFields.remove(s);
return oldValue;
}
return null;
}
@Override
public Set<Entry<String, List<String>>> entrySet()
{
return new AbstractSet<>()
{
@Override
public Iterator<Entry<String, List<String>>> iterator()
{
return new Iterator<>()
{
private final Iterator<String> iterator = httpFields.getFieldNamesCollection().iterator();
private String name = null;
@Override
public boolean hasNext()
{
return iterator.hasNext();
}
@Override
public Entry<String, List<String>> next()
{
name = iterator.next();
return new HttpFieldsEntry(name);
}
@Override
public void remove()
{
if (name != null)
{
HttpFieldsMap.this.remove(name);
name = null;
}
}
};
}
@Override
public int size()
{
return httpFields.getFieldNamesCollection().size();
}
};
}
private class HttpFieldsEntry implements Entry<String, List<String>>
{
private final String _name;
public HttpFieldsEntry(String name)
{
_name = name;
}
@Override
public String getKey()
{
return _name;
}
@Override
public List<String> getValue()
{
return HttpFieldsMap.this.get(_name);
}
@Override
public List<String> setValue(List<String> value)
{
return HttpFieldsMap.this.put(_name, value);
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o instanceof HttpFieldsEntry other)
return StringUtil.asciiEqualsIgnoreCase(_name, other.getKey());
return false;
}
@Override
public int hashCode()
{
return Objects.hash(StringUtil.asciiToLowerCase(_name));
}
}
}

View File

@ -79,7 +79,7 @@ public class DelegatedJettyClientUpgradeRequest implements UpgradeRequest
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(delegate.getHeaders()));
return HttpFields.asMap(delegate.getHeaders());
}
@Override

View File

@ -65,7 +65,7 @@ public class DelegatedJettyClientUpgradeResponse implements UpgradeResponse
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(delegate.getHeaders()));
return HttpFields.asMap(delegate.getHeaders());
}
@Override

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.websocket.server.internal;
import java.net.HttpCookie;
import java.net.URI;
import java.security.Principal;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -73,7 +72,7 @@ class UpgradeRequestDelegate implements UpgradeRequest
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(request.getHeaders()));
return HttpFields.asMap(request.getHeaders());
}
@Override

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.ee10.websocket.jakarta.client.internal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -39,16 +38,11 @@ public class JsrUpgradeListener implements UpgradeListener
if (configurator == null)
return;
// Give headers to configurator
HttpFields fields = request.getHeaders();
Map<String, List<String>> originalHeaders = HttpFields.asMap(fields);
configurator.beforeRequest(originalHeaders);
// Reset headers on HttpRequest per configurator
request.headers(headers ->
{
headers.clear();
originalHeaders.forEach(headers::put);
// Give headers to configurator
Map<String, List<String>> headersMap = HttpFields.asMap(headers);
configurator.beforeRequest(headersMap);
});
}
@ -58,7 +52,7 @@ public class JsrUpgradeListener implements UpgradeListener
if (configurator == null)
return;
HandshakeResponse handshakeResponse = () -> Collections.unmodifiableMap(HttpFields.asMap(response.getHeaders()));
HandshakeResponse handshakeResponse = () -> HttpFields.asMap(response.getHeaders());
configurator.afterResponse(handshakeResponse);
}
}

View File

@ -156,8 +156,6 @@ public class JakartaWebSocketCreator implements WebSocketCreator
// [JSR] Step 5: Call modifyHandshake
configurator.modifyHandshake(config, jsrHandshakeRequest, jsrHandshakeResponse);
// Set modified headers Map back into response properly
jsrHandshakeResponse.setHeaders(jsrHandshakeResponse.getHeaders());
try
{

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.ee10.websocket.jakarta.server.internal;
import java.net.URI;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -46,7 +45,7 @@ public class JsrHandshakeRequest implements HandshakeRequest
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(delegate.getHeaders()));
return HttpFields.asMap(delegate.getHeaders());
}
@Override

View File

@ -22,13 +22,11 @@ import org.eclipse.jetty.websocket.core.server.ServerUpgradeResponse;
public class JsrHandshakeResponse implements HandshakeResponse
{
private final ServerUpgradeResponse delegate;
private final Map<String, List<String>> headers;
public JsrHandshakeResponse(ServerUpgradeResponse resp)
{
this.delegate = resp;
this.headers = HttpFields.asMap(delegate.getHeaders());
this.headers = HttpFields.asMap(resp.getHeaders());
}
@Override
@ -36,9 +34,4 @@ public class JsrHandshakeResponse implements HandshakeResponse
{
return headers;
}
public void setHeaders(Map<String, List<String>> headers)
{
headers.forEach((key, values) -> delegate.getHeaders().put(key, values));
}
}

View File

@ -121,7 +121,7 @@ public class DelegatedServerUpgradeRequest implements JettyServerUpgradeRequest
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(upgradeRequest.getHeaders()));
return HttpFields.asMap(upgradeRequest.getHeaders());
}
@Override

View File

@ -0,0 +1,124 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.websocket.tests;
import java.net.URI;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator;
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.JettyUpgradeListener;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class UpgradeHeadersTest
{
private Server _server;
private WebSocketClient _client;
private ServerConnector _connector;
public void start(JettyWebSocketCreator creator) throws Exception
{
_server = new Server();
_connector = new ServerConnector(_server);
_server.addConnector(_connector);
ServletContextHandler contextHandler = new ServletContextHandler();
JettyWebSocketServletContainerInitializer.configure(contextHandler, (servletContext, container) ->
container.addMapping("/", creator));
_server.setHandler(contextHandler);
_server.start();
_client = new WebSocketClient();
_client.start();
}
@AfterEach
public void after() throws Exception
{
_client.stop();
_server.stop();
}
@Test
public void testCaseInsensitiveUpgradeHeaders() throws Exception
{
start((request, response) ->
{
// Verify that existing headers can be accessed in a case-insensitive way.
if (request.getHeaders().get("cOnnEcTiOn") == null)
throw new IllegalStateException("No Connection Header on HandshakeRequest");
if (response.getHeaders().get("sErVeR") == null)
throw new IllegalStateException("No Server Header on HandshakeResponse");
// Verify custom header sent from client.
if (request.getHeaders().get("SeNtHeadEr") == null)
throw new IllegalStateException("No sent Header on HandshakeResponse");
// Add custom response header.
response.getHeaders().put("myHeader", List.of("foobar"));
if (response.getHeaders().get("MyHeAdEr") == null)
throw new IllegalStateException("No custom Header on HandshakeResponse");
return new EchoSocket();
});
EventSocket clientEndpoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + _connector.getLocalPort());
ClientUpgradeRequest clientUpgradeRequest = new ClientUpgradeRequest();
clientUpgradeRequest.getHeaders().put("sentHeader", List.of("value123"));
if (clientUpgradeRequest.getHeaders().get("SenTHeaDer") == null)
throw new IllegalStateException("No custom Header on ClientUpgradeRequest");
JettyUpgradeListener upgradeListener = new JettyUpgradeListener()
{
@Override
public void onHandshakeRequest(Request request)
{
// Verify that existing headers can be accessed in a case-insensitive way.
if (request.getHeaders().get("cOnnEcTiOn") == null)
throw new IllegalStateException("No Connection Header on client Request");
if (request.getHeaders().get("SenTHeaDer") == null)
throw new IllegalStateException("No custom Header on ClientUpgradeRequest");
}
@Override
public void onHandshakeResponse(Request request, Response response)
{
if (response.getHeaders().get("MyHeAdEr") == null)
throw new IllegalStateException("No custom Header on HandshakeResponse");
if (response.getHeaders().get("cOnnEcTiOn") == null)
throw new IllegalStateException("No Connection Header on HandshakeRequest");
}
};
// If any of the above throw it would fail to upgrade to websocket.
assertNotNull(_client.connect(clientEndpoint, uri, clientUpgradeRequest, upgradeListener).get(5, TimeUnit.SECONDS));
assertTrue(clientEndpoint.openLatch.await(5, TimeUnit.SECONDS));
clientEndpoint.session.close();
assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
}
}

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.ee9.websocket.jakarta.client.internal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -39,16 +38,11 @@ public class JsrUpgradeListener implements UpgradeListener
if (configurator == null)
return;
// Give headers to configurator
HttpFields fields = request.getHeaders();
Map<String, List<String>> originalHeaders = HttpFields.asMap(fields);
configurator.beforeRequest(originalHeaders);
// Reset headers on HttpRequest per configurator
request.headers(headers ->
{
headers.clear();
originalHeaders.forEach(headers::put);
// Give headers to configurator
Map<String, List<String>> headersMap = HttpFields.asMap(headers);
configurator.beforeRequest(headersMap);
});
}
@ -58,7 +52,7 @@ public class JsrUpgradeListener implements UpgradeListener
if (configurator == null)
return;
HandshakeResponse handshakeResponse = () -> Collections.unmodifiableMap(HttpFields.asMap(response.getHeaders()));
HandshakeResponse handshakeResponse = () -> HttpFields.asMap(response.getHeaders());
configurator.afterResponse(handshakeResponse);
}
}

View File

@ -156,8 +156,6 @@ public class JakartaWebSocketCreator implements WebSocketCreator
// [JSR] Step 5: Call modifyHandshake
configurator.modifyHandshake(config, jsrHandshakeRequest, jsrHandshakeResponse);
// Set modified headers Map back into response properly
jsrHandshakeResponse.setHeaders(jsrHandshakeResponse.getHeaders());
try
{

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.ee9.websocket.jakarta.server.internal;
import java.net.URI;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -46,7 +45,7 @@ public class JsrHandshakeRequest implements HandshakeRequest
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(delegate.getHeaders()));
return HttpFields.asMap(delegate.getHeaders());
}
@Override

View File

@ -22,13 +22,11 @@ import org.eclipse.jetty.websocket.core.server.ServerUpgradeResponse;
public class JsrHandshakeResponse implements HandshakeResponse
{
private final ServerUpgradeResponse delegate;
private final Map<String, List<String>> headers;
public JsrHandshakeResponse(ServerUpgradeResponse resp)
{
this.delegate = resp;
this.headers = HttpFields.asMap(delegate.getHeaders());
this.headers = HttpFields.asMap(resp.getHeaders());
}
@Override
@ -36,9 +34,4 @@ public class JsrHandshakeResponse implements HandshakeResponse
{
return headers;
}
public void setHeaders(Map<String, List<String>> headers)
{
headers.forEach((key, values) -> delegate.getHeaders().put(key, values));
}
}

View File

@ -79,7 +79,7 @@ public class DelegatedJettyClientUpgradeRequest implements UpgradeRequest
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(delegate.getHeaders()));
return HttpFields.asMap(delegate.getHeaders());
}
@Override

View File

@ -65,7 +65,7 @@ public class DelegatedJettyClientUpgradeResponse implements UpgradeResponse
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(delegate.getHeaders()));
return HttpFields.asMap(delegate.getHeaders());
}
@Override

View File

@ -314,7 +314,6 @@ public abstract class JettyWebSocketServlet extends HttpServlet
try
{
Object webSocket = creator.createWebSocket(request, response);
response.copyHeaders();
if (webSocket == null)
callback.succeeded();
return webSocket;

View File

@ -114,7 +114,7 @@ public class DelegatedServerUpgradeRequest implements JettyServerUpgradeRequest
@Override
public Map<String, List<String>> getHeaders()
{
return Collections.unmodifiableMap(HttpFields.asMap(upgradeRequest.getHeaders()));
return HttpFields.asMap(upgradeRequest.getHeaders());
}
@Override

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.ee9.websocket.server.internal;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -33,17 +32,10 @@ public class DelegatedServerUpgradeResponse implements JettyServerUpgradeRespons
{
private final ServerUpgradeResponse upgradeResponse;
private final HttpServletResponse httpServletResponse;
private final boolean isUpgraded;
public DelegatedServerUpgradeResponse(ServerUpgradeResponse response)
{
this(response, false);
}
public DelegatedServerUpgradeResponse(ServerUpgradeResponse response, boolean isUpgraded)
{
this.upgradeResponse = response;
this.isUpgraded = isUpgraded;
this.httpServletResponse = (HttpServletResponse)response.getRequest()
.getAttribute(WebSocketConstants.WEBSOCKET_WRAPPED_RESPONSE_ATTRIBUTE);
}
@ -56,11 +48,6 @@ public class DelegatedServerUpgradeResponse implements JettyServerUpgradeRespons
upgradeResponse.getHeaders().add(name, value);
}
public void copyHeaders()
{
headers.forEach((key, values) -> upgradeResponse.getHeaders().put(key, values));
}
@Override
public void setHeader(String name, String value)
{
@ -100,10 +87,7 @@ public class DelegatedServerUpgradeResponse implements JettyServerUpgradeRespons
@Override
public Map<String, List<String>> getHeaders()
{
if (isUpgraded)
return Collections.unmodifiableMap(HttpFields.asMap(upgradeResponse.getHeaders()));
else
return HttpFields.asMap(upgradeResponse.getHeaders());
return HttpFields.asMap(upgradeResponse.getHeaders());
}
@Override

View File

@ -41,7 +41,7 @@ public class JettyServerFrameHandlerFactory extends JettyWebSocketFrameHandlerFa
{
JettyWebSocketFrameHandler frameHandler = super.newJettyFrameHandler(websocketPojo);
frameHandler.setUpgradeRequest(new DelegatedServerUpgradeRequest(upgradeRequest));
frameHandler.setUpgradeResponse(new DelegatedServerUpgradeResponse(upgradeResponse, true));
frameHandler.setUpgradeResponse(new DelegatedServerUpgradeResponse(upgradeResponse));
return frameHandler;
}
}

View File

@ -0,0 +1,124 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee9.websocket.tests;
import java.net.URI;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
import org.eclipse.jetty.ee9.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.ee9.websocket.client.JettyUpgradeListener;
import org.eclipse.jetty.ee9.websocket.client.WebSocketClient;
import org.eclipse.jetty.ee9.websocket.server.JettyWebSocketCreator;
import org.eclipse.jetty.ee9.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class UpgradeHeadersTest
{
private Server _server;
private WebSocketClient _client;
private ServerConnector _connector;
public void start(JettyWebSocketCreator creator) throws Exception
{
_server = new Server();
_connector = new ServerConnector(_server);
_server.addConnector(_connector);
ServletContextHandler contextHandler = new ServletContextHandler();
JettyWebSocketServletContainerInitializer.configure(contextHandler, (servletContext, container) ->
container.addMapping("/", creator));
_server.setHandler(contextHandler);
_server.start();
_client = new WebSocketClient();
_client.start();
}
@AfterEach
public void after() throws Exception
{
_client.stop();
_server.stop();
}
@Test
public void testCaseInsensitiveUpgradeHeaders() throws Exception
{
start((request, response) ->
{
// Verify that existing headers can be accessed in a case-insensitive way.
if (request.getHeaders().get("cOnnEcTiOn") == null)
throw new IllegalStateException("No Connection Header on HandshakeRequest");
if (response.getHeaders().get("sErVeR") == null)
throw new IllegalStateException("No Server Header on HandshakeResponse");
// Verify custom header sent from client.
if (request.getHeaders().get("SeNtHeadEr") == null)
throw new IllegalStateException("No sent Header on HandshakeResponse");
// Add custom response header.
response.getHeaders().put("myHeader", List.of("foobar"));
if (response.getHeaders().get("MyHeAdEr") == null)
throw new IllegalStateException("No custom Header on HandshakeResponse");
return new EchoSocket();
});
EventSocket clientEndpoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + _connector.getLocalPort());
ClientUpgradeRequest clientUpgradeRequest = new ClientUpgradeRequest();
clientUpgradeRequest.getHeaders().put("sentHeader", List.of("value123"));
if (clientUpgradeRequest.getHeaders().get("SenTHeaDer") == null)
throw new IllegalStateException("No custom Header on ClientUpgradeRequest");
JettyUpgradeListener upgradeListener = new JettyUpgradeListener()
{
@Override
public void onHandshakeRequest(Request request)
{
// Verify that existing headers can be accessed in a case-insensitive way.
if (request.getHeaders().get("cOnnEcTiOn") == null)
throw new IllegalStateException("No Connection Header on client Request");
if (request.getHeaders().get("SenTHeaDer") == null)
throw new IllegalStateException("No custom Header on ClientUpgradeRequest");
}
@Override
public void onHandshakeResponse(Request request, Response response)
{
if (response.getHeaders().get("MyHeAdEr") == null)
throw new IllegalStateException("No custom Header on HandshakeResponse");
if (response.getHeaders().get("cOnnEcTiOn") == null)
throw new IllegalStateException("No Connection Header on HandshakeRequest");
}
};
// If any of the above throw it would fail to upgrade to websocket.
assertNotNull(_client.connect(clientEndpoint, uri, clientUpgradeRequest, upgradeListener).get(5, TimeUnit.SECONDS));
assertTrue(clientEndpoint.openLatch.await(5, TimeUnit.SECONDS));
clientEndpoint.session.close();
assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
}
}