diff --git a/core/src/main/java/com/predic8/membrane/core/http/Request.java b/core/src/main/java/com/predic8/membrane/core/http/Request.java index c5c0b3f8cb..5bb5c54771 100644 --- a/core/src/main/java/com/predic8/membrane/core/http/Request.java +++ b/core/src/main/java/com/predic8/membrane/core/http/Request.java @@ -31,283 +31,270 @@ public class Request extends Message { - private static final Pattern pattern = Pattern.compile("(.+?) (.+?) HTTP/(.+?)$"); - private static final Pattern stompPattern = Pattern.compile("^(.+?)$"); - - public static final String METHOD_GET = "GET"; - public static final String METHOD_POST = "POST"; - public static final String METHOD_PATCH = "PATCH"; - public static final String METHOD_HEAD = "HEAD"; - public static final String METHOD_DELETE = "DELETE"; - public static final String METHOD_PUT = "PUT"; - @SuppressWarnings("unused") - public static final String METHOD_TRACE = "TRACE"; - public static final String METHOD_CONNECT = "CONNECT"; - public static final String METHOD_OPTIONS = "OPTIONS"; - - private static final HashSet methodsWithoutBody = Sets.newHashSet(METHOD_GET, METHOD_HEAD, METHOD_CONNECT); - private static final HashSet methodsWithOptionalBody = Sets.newHashSet( - METHOD_POST, - METHOD_DELETE, - /* some WebDAV methods, see http://www.ietf.org/rfc/rfc2518.txt */ - METHOD_OPTIONS, - "PROPFIND", - "MKCOL", - "COPY", - "MOVE", - "LOCK", - "UNLOCK"); - - - String method; - String uri; - - @Override - protected void parseStartLine(InputStream in) throws IOException { - try { - String firstLine = HttpUtil.readLine(in); - Matcher matcher = pattern.matcher(firstLine); - if (matcher.find()) { - method = matcher.group(1); - uri = matcher.group(2); - version = matcher.group(3); - } else if (stompPattern.matcher(firstLine).find()) { - method = firstLine; - uri = ""; - version = "STOMP"; - } else { - throw new EOFWhileReadingFirstLineException(firstLine); - } - } catch (EOFWhileReadingLineException e) { - if (e.getLineSoFar().isEmpty()) - throw new NoMoreRequestsException(); // happens regularly at the end of a keep-alive connection - throw new EOFWhileReadingFirstLineException(e.getLineSoFar()); - } - } - - public String getMethod() { - return method; - } - - public void setMethod(String method) { - this.method = method; - } - - /** - * @return the "Request-URI" as sent by the client in the first line of the HTTP request (quoting from RFC 2616: Request-URI = "*" | absoluteURI | - * abs_path | authority ) - */ - public String getUri() { - return uri; - } - - public void setUri(String uri) { - this.uri = uri; - } - - @Override - public String getStartLine() { - return method + " " + uri + " HTTP/" + version + CRLF; - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isHEADRequest() { - return METHOD_HEAD.equals(method); - } - - public boolean isGETRequest() { - return METHOD_GET.equals(method); - } - - public boolean isPOSTRequest() { - return METHOD_POST.equals(method); - } - - public boolean isDELETERequest() { - return METHOD_DELETE.equals(method); - } - - public boolean isCONNECTRequest() { - return METHOD_CONNECT.equals(method); - } - - public boolean isOPTIONSRequest() { - return METHOD_OPTIONS.equals(method); - } - - @Override - public String getName() { - return uri; - } - - @Override - public boolean shouldNotContainBody() { - if (methodsWithoutBody.contains(method)) - return true; - if (methodsWithOptionalBody.contains(method)) { - if (header.hasContentLength()) - return header.getContentLength() == 0; - return header.getFirstValue(TRANSFER_ENCODING) == null; + private static final Pattern pattern = Pattern.compile("(.+?) (.+?) HTTP/(.+?)$"); + private static final Pattern stompPattern = Pattern.compile("^(.+?)$"); + + public static final String METHOD_GET = "GET"; + public static final String METHOD_POST = "POST"; + public static final String METHOD_PATCH = "PATCH"; + public static final String METHOD_HEAD = "HEAD"; + public static final String METHOD_DELETE = "DELETE"; + public static final String METHOD_PUT = "PUT"; + @SuppressWarnings("unused") + public static final String METHOD_TRACE = "TRACE"; + public static final String METHOD_CONNECT = "CONNECT"; + public static final String METHOD_OPTIONS = "OPTIONS"; + + private static final HashSet methodsWithoutBody = Sets.newHashSet(METHOD_GET, METHOD_HEAD, METHOD_CONNECT); + + private String method; + private String uri; + + @Override + protected void parseStartLine(InputStream in) throws IOException { + try { + String firstLine = HttpUtil.readLine(in); + Matcher matcher = pattern.matcher(firstLine); + if (matcher.find()) { + method = matcher.group(1); + uri = matcher.group(2); + version = matcher.group(3); + } else if (stompPattern.matcher(firstLine).find()) { + method = firstLine; + uri = ""; + version = "STOMP"; + } else { + throw new EOFWhileReadingFirstLineException(firstLine); + } + } catch (EOFWhileReadingLineException e) { + if (e.getLineSoFar().isEmpty()) + throw new NoMoreRequestsException(); // happens regularly at the end of a keep-alive connection + throw new EOFWhileReadingFirstLineException(e.getLineSoFar()); + } + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + /** + * @return the "Request-URI" as sent by the client in the first line of the HTTP request (quoting from RFC 2616: Request-URI = "*" | absoluteURI | + * abs_path | authority ) + */ + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + @Override + public String getStartLine() { + return method + " " + uri + " HTTP/" + version + CRLF; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isHEADRequest() { + return METHOD_HEAD.equals(method); + } + + public boolean isGETRequest() { + return METHOD_GET.equals(method); + } + + public boolean isPOSTRequest() { + return METHOD_POST.equals(method); + } + + public boolean isDELETERequest() { + return METHOD_DELETE.equals(method); + } + + public boolean isCONNECTRequest() { + return METHOD_CONNECT.equals(method); + } + + public boolean isOPTIONSRequest() { + return METHOD_OPTIONS.equals(method); + } + + @Override + public String getName() { + return uri; + } + + @Override + public boolean shouldNotContainBody() { + if (methodsWithoutBody.contains(method)) // GET, HEAD, CONNECT + return true; + if (isHTTP10()) + return false; + if (header.hasContentLength()) + return header.getContentLength() == 0; + return header.getFirstValue(TRANSFER_ENCODING) == null; + } + + /** + * NTLM and SPNEGO authentication schemes authorize HTTP connections, not single requests. + * We therefore have to "bind" the targetConnection to the incoming connection to ensure + * the same targetConnection is used again for further requests. + */ + public boolean isBindTargetConnectionToIncoming() { + String auth = header.getFirstValue(AUTHORIZATION); + return auth != null && (auth.startsWith("NTLM") || auth.startsWith("Negotiate")); + } + + @Override + public int estimateHeapSize() { + return super.estimateHeapSize() + + 12 + + (method != null ? 2 * method.length() : 0) + + (uri != null ? 2 * uri.length() : 0); + } + + @Override + public T createSnapshot(Runnable bodyUpdatedCallback, BodyCollectingMessageObserver.Strategy strategy, long limit) { + var result = createMessageSnapshot(new Request(), bodyUpdatedCallback, strategy, limit); + result.setUri(getUri()); + result.setMethod(getMethod()); + return (T) result; + } + + public final void writeSTOMP(OutputStream out, boolean retainBody) throws IOException { + out.write(getMethod().getBytes(UTF_8)); + out.write(10); + for (HeaderField hf : header.getAllHeaderFields()) + out.write(("%s:%s\n".formatted(hf.getHeaderName().toString(), hf.getValue())).getBytes(UTF_8)); + out.write(10); + body.write(new PlainBodyTransferer(out), retainBody); + } + + public static Builder get(String url) throws URISyntaxException { + return new Builder().get(url); + } + + public static Builder put(String url) throws URISyntaxException { + return new Builder().put(url); + } + + public static Builder post(String url) throws URISyntaxException { + return new Builder().post(url); + } + + public static Builder delete(String url) throws URISyntaxException { + return new Builder().delete(url); + } + + public static Builder options(String url) throws URISyntaxException { + return new Builder().options(url); + } + + public static Builder connect(String url) throws URISyntaxException { + return new Builder().connect(url); + } + + public static class Builder { + private final Request req; + private String fullURL; + + public Builder() { + req = new Request(); + req.setVersion("1.1"); + } + + public Request build() { + return req; + } + + public Exchange buildExchange() { + return buildExchange(null); + } + + public Exchange buildExchange(AbstractHttpHandler handler) { + Exchange exc = new Exchange(handler); + Request req = build(); + exc.setRequest(req); + exc.getDestinations().add(fullURL); + exc.setOriginalRequestUri(req.getUri()); + return exc; + } + + public Builder method(String method) { + req.setMethod(method); + return this; + } + + public Builder url(URIFactory uriFactory, String url) throws URISyntaxException { + fullURL = url; + req.setUri(URLUtil.getPathQuery(uriFactory, url)); + return this; + } + + public Builder uri(String uri) { + req.setUri(uri); + return this; + } + + public Builder version(String version) { + req.setVersion(version); + return this; + } + + public Builder authorization(String username, String password) { + req.getHeader().setAuthorization(username, password); + return this; + } + + public Builder header(String headerName, String headerValue) { + req.getHeader().add(headerName, headerValue); + return this; + } + + public Builder contentType(String value) { + req.getHeader().setContentType( value); + return this; + } + + public Builder header(Header headers) { + req.setHeader(headers); + return this; + } + + public Builder body(String body) { + req.setBodyContent(body.getBytes(UTF_8)); + return this; + } + + public Builder body(InputStream is) { + req.setBody(new Body(is)); + return this; + } + + public Builder body(byte[] body) { + req.setBodyContent(body); + return this; + } + + public Builder body(long contentLength, InputStream body) { + req.body = new Body(body, contentLength); + Header header = req.getHeader(); + header.removeFields(CONTENT_ENCODING); + header.removeFields(TRANSFER_ENCODING); + header.setContentLength(contentLength); + return this; + } + + public Builder json(String body) { + req.setBodyContent(body.getBytes(UTF_8)); + req.header.setContentType(APPLICATION_JSON); + return this; } - return false; - } - - /** - * NTLM and SPNEGO authentication schemes authorize HTTP connections, not single requests. - * We therefore have to "bind" the targetConnection to the incoming connection to ensure - * the same targetConnection is used again for further requests. - */ - public boolean isBindTargetConnectionToIncoming() { - String auth = header.getFirstValue(AUTHORIZATION); - return auth != null && (auth.startsWith("NTLM") || auth.startsWith("Negotiate")); - } - - @Override - public int estimateHeapSize() { - return super.estimateHeapSize() + - 12 + - (method != null ? 2*method.length() : 0) + - (uri != null ? 2*uri.length() : 0); - } - - @Override - public T createSnapshot(Runnable bodyUpdatedCallback, BodyCollectingMessageObserver.Strategy strategy, long limit) { - var result = createMessageSnapshot(new Request(), bodyUpdatedCallback, strategy, limit); - result.setUri(getUri()); - result.setMethod(getMethod()); - return (T) result; - } - - public final void writeSTOMP(OutputStream out, boolean retainBody) throws IOException { - out.write(getMethod().getBytes(UTF_8)); - out.write(10); - for (HeaderField hf : header.getAllHeaderFields()) - out.write(("%s:%s\n".formatted(hf.getHeaderName().toString(), hf.getValue())).getBytes(UTF_8)); - out.write(10); - body.write(new PlainBodyTransferer(out), retainBody); - } - - public static Builder get(String url) throws URISyntaxException { - return new Builder().get(url); - } - - public static Builder put(String url) throws URISyntaxException { - return new Builder().put(url); - } - - public static Builder post(String url) throws URISyntaxException { - return new Builder().post(url); - } - - public static Builder delete(String url) throws URISyntaxException { - return new Builder().delete(url); - } - - public static Builder options(String url) throws URISyntaxException { - return new Builder().options(url); - } - - public static Builder connect(String url) throws URISyntaxException { - return new Builder().connect(url); - } - - public static class Builder { - private final Request req; - private String fullURL; - - public Builder() { - req = new Request(); - req.setVersion("1.1"); - } - - public Request build() { - return req; - } - - public Exchange buildExchange() { - return buildExchange(null); - } - - public Exchange buildExchange(AbstractHttpHandler handler) { - Exchange exc = new Exchange(handler); - Request req = build(); - exc.setRequest(req); - exc.getDestinations().add(fullURL); - exc.setOriginalRequestUri(req.getUri()); - return exc; - } - - public Builder method(String method) { - req.setMethod(method); - return this; - } - - public Builder url(URIFactory uriFactory, String url) throws URISyntaxException { - fullURL = url; - req.setUri(URLUtil.getPathQuery(uriFactory, url)); - return this; - } - - public Builder uri(String uri) { - req.setUri(uri); - return this; - } - - public Builder version(String version) { - req.setVersion(version); - return this; - } - - public Builder authorization(String username, String password) { - req.getHeader().setAuthorization(username,password); - return this; - } - - public Builder header(String headerName, String headerValue) { - req.getHeader().add(headerName, headerValue); - return this; - } - - public Builder contentType(String value) { - req.getHeader().add(CONTENT_TYPE, value); - return this; - } - - public Builder header(Header headers) { - req.setHeader(headers); - return this; - } - - public Builder body(String body) { - req.setBodyContent(body.getBytes(UTF_8)); - return this; - } - - public Builder body(InputStream is) { - req.setBody(new Body(is)); - return this; - } - - public Builder body(byte[] body) { - req.setBodyContent(body); - return this; - } - - public Builder body(long contentLength, InputStream body) { - req.body = new Body(body, contentLength); - Header header = req.getHeader(); - header.removeFields(CONTENT_ENCODING); - header.removeFields(TRANSFER_ENCODING); - header.setContentLength(contentLength); - return this; - } - - public Builder json(String body) { - req.setBodyContent(body.getBytes(UTF_8)); - req.header.setContentType(APPLICATION_JSON); - return this; - } public Builder xml(String body) { req.setBodyContent(body.getBytes(UTF_8)); @@ -315,53 +302,59 @@ public Builder xml(String body) { return this; } - public Builder post(URIFactory uriFactory, String url) throws URISyntaxException { - return method(Request.METHOD_POST).url(uriFactory, url); - } - - public Builder post(String url) throws URISyntaxException { - return post(new URIFactory(), url); - } - - public Builder get(URIFactory uriFactory, String url) throws URISyntaxException { - return method(METHOD_GET).url(uriFactory, url); - } - - /** - * Sets the request's method to "GET" and the URI to the parameter. Uses a standard {@link URIFactory}. - */ - public Builder get(String url) throws URISyntaxException { - return get(new URIFactory(), url); - } - - public Builder delete(URIFactory uriFactory, String url) throws URISyntaxException { - return method(Request.METHOD_DELETE).url(uriFactory, url); - } - - public Builder delete(String url) throws URISyntaxException { - return delete(new URIFactory(), url); - } - - public Builder put(URIFactory uriFactory, String url) throws URISyntaxException { - return method(Request.METHOD_PUT).url(uriFactory, url); - } - - public Builder put(String url) throws URISyntaxException { - return put(new URIFactory(), url); - } - - public Builder options(String url) throws URISyntaxException { - return options(new URIFactory(), url); - } - - public Builder connect(String url) throws URISyntaxException { - req.setMethod(METHOD_CONNECT); + public Builder post(URIFactory uriFactory, String url) throws URISyntaxException { + return method(Request.METHOD_POST).url(uriFactory, url); + } + + /** + * For tests and special cases where URIs do not contain weird characters. + * @param url + * @return + * @throws URISyntaxException + */ + public Builder post(String url) throws URISyntaxException { + return post(new URIFactory(), url); + } + + public Builder get(URIFactory uriFactory, String url) throws URISyntaxException { + return method(METHOD_GET).url(uriFactory, url); + } + + /** + * Sets the request's method to "GET" and the URI to the parameter. Uses a standard {@link URIFactory}. + */ + public Builder get(String url) throws URISyntaxException { + return get(new URIFactory(), url); + } + + public Builder delete(URIFactory uriFactory, String url) throws URISyntaxException { + return method(Request.METHOD_DELETE).url(uriFactory, url); + } + + public Builder delete(String url) throws URISyntaxException { + return delete(new URIFactory(), url); + } + + public Builder put(URIFactory uriFactory, String url) throws URISyntaxException { + return method(Request.METHOD_PUT).url(uriFactory, url); + } + + public Builder put(String url) throws URISyntaxException { + return put(new URIFactory(), url); + } + + public Builder options(String url) throws URISyntaxException { + return options(new URIFactory(), url); + } + + public Builder connect(String url) throws URISyntaxException { + req.setMethod(METHOD_CONNECT); req.setUri(new URIFactory().create(url).getAuthority()); - return this; - } + return this; + } - public Builder options(URIFactory uriFactory, String url) throws URISyntaxException { - return method(Request.METHOD_OPTIONS).url(uriFactory,url); - } - } + public Builder options(URIFactory uriFactory, String url) throws URISyntaxException { + return method(Request.METHOD_OPTIONS).url(uriFactory, url); + } + } } diff --git a/core/src/test/java/com/predic8/membrane/core/http/Http10Test.java b/core/src/test/java/com/predic8/membrane/core/http/Http10Test.java index 430b56d592..e22d895742 100644 --- a/core/src/test/java/com/predic8/membrane/core/http/Http10Test.java +++ b/core/src/test/java/com/predic8/membrane/core/http/Http10Test.java @@ -33,7 +33,7 @@ public class Http10Test { private static Router router2; @BeforeAll - public static void setUp() throws Exception { + static void setUp() throws Exception { ServiceProxy proxy2 = new ServiceProxy(new ServiceProxyKey("localhost", "POST", ".*", 2000), null, 0); proxy2.getFlow().add(new SampleSoapServiceInterceptor()); router2 = new TestRouter(); @@ -48,7 +48,7 @@ public static void setUp() throws Exception { } @AfterAll - public static void tearDown() { + static void tearDown() { router2.stop(); router.stop(); } @@ -82,7 +82,7 @@ void post() throws Exception { @Test - void testMultiplePost() throws Exception { + void multiplePost() throws Exception { HttpClient client = new HttpClient(); client.getParams().setParameter(PROTOCOL_VERSION, HTTP_1_0); diff --git a/core/src/test/java/com/predic8/membrane/core/http/RequestTest.java b/core/src/test/java/com/predic8/membrane/core/http/RequestTest.java index be4fe3ecaf..5b6ee5b9f2 100644 --- a/core/src/test/java/com/predic8/membrane/core/http/RequestTest.java +++ b/core/src/test/java/com/predic8/membrane/core/http/RequestTest.java @@ -44,6 +44,7 @@ public class RequestTest { Content-Type: application/x-www-form-urlencoded endpoint=http%3A%2F%2Fwww.thomas-bayer.com%3A80%2Faxis2%2Fservices%2FBLZService&xpath%3A%2FgetBank%2Fblz=38070024&id=65657&operation=getBank&portType=BLZServicePortType"""; + private static final String CHUNKED = """ POST /axis2/services/BLZService HTTP/1.1 Content-Type: application/soap+xml; charset=UTF-8; action="http://thomas-bayer.com/blz/BLZServicePortType/getBankRequest" @@ -53,14 +54,19 @@ public class RequestTest { ff 66762332 0"""; - /** - * No body related headers - */ - private static final String EMPTY_BODY = """ + + private static final String POST_EMPTY_BODY = """ POST /operation/call HTTP/1.1 Host: api.predic8.de:443 """; + + private static final String PATCH_EMPTY_BODY = """ + PATCH /operation/call HTTP/1.1 + Host: api.predic8.de:443 + + """; + private static final String EMPTY_BODY_CONTENT_LENGTH = """ POST /operation/call HTTP/1.1 Host: api.predic8.de:443 @@ -74,7 +80,6 @@ public class RequestTest { """; // Trailing line is needed for chunked parsing - @SuppressWarnings("TrailingWhitespacesInTextBlock") private static final String NO_CHUNKS = """ POST /resource HTTP/1.1 Host: example.com @@ -83,16 +88,32 @@ public class RequestTest { 0 """; + + private static final String http10NoBody = """ + POST /operation/call HTTP/1.0 + Host: api.predic8.de + + """; + + private static final String http10Body = """ + POST /operation/call HTTP/1.0 + Host: api.predic8.de + + Foo"""; + private Request request; private InputStream getReq; private InputStream inPost; - private InputStream inEmptyBody; + private InputStream inPostEmptyBody; + private InputStream inPatchEmptyBody; private InputStream inEmptyBodyContentLength; private InputStream inEmptyBodyContentType; private InputStream inNoChunks; private InputStream inChunked; private ByteArrayOutputStream tempOut; private InputStream tempIn; + private InputStream inHttp10NoBody; + private InputStream inHttp10Body; private static void shouldBodyBeRead(String message, boolean expect) throws IOException, EndOfStreamException { Request req = new Request(); @@ -101,28 +122,34 @@ private static void shouldBodyBeRead(String message, boolean expect) throws IOEx } @BeforeEach - public void setUp() { + void setUp() { request = new Request(); getReq = convertMessage(GET); inPost = convertMessage(POST); - inEmptyBody = convertMessage(EMPTY_BODY); + inPostEmptyBody = convertMessage(POST_EMPTY_BODY); + inPatchEmptyBody = convertMessage(PATCH_EMPTY_BODY); inEmptyBodyContentLength = convertMessage(EMPTY_BODY_CONTENT_LENGTH); inEmptyBodyContentType = convertMessage(EMPTY_BODY_CONTENT_TYPE); inNoChunks = convertMessage(NO_CHUNKS); inChunked = convertMessage(CHUNKED); + + inHttp10NoBody = convertMessage(http10NoBody); + inHttp10Body = convertMessage(http10Body); } @AfterEach - public void tearDown() throws Exception { + void tearDown() { closeQuietly(getReq); closeQuietly(inPost); - closeQuietly(inEmptyBody); + closeQuietly(inPostEmptyBody); closeQuietly(inEmptyBodyContentLength); closeQuietly(inEmptyBodyContentType); closeQuietly(inNoChunks); closeQuietly(inChunked); closeQuietly(tempIn); closeQuietly(tempOut); + closeQuietly(inHttp10Body); + closeQuietly(inHttp10NoBody); } @Test @@ -163,7 +190,12 @@ void noChunks() throws Exception { @Test void emptyBody() throws Exception { - testForEmptyBody(inEmptyBody); + testForEmptyBody(inPostEmptyBody); + } + + @Test + void emptyBodyPatch() throws Exception { + testForEmptyBody(inPatchEmptyBody); } @Test @@ -176,9 +208,8 @@ void emptyBodyContentType() throws Exception { testForEmptyBody(inEmptyBodyContentType); } - private void testForEmptyBody(InputStream message) throws IOException, EndOfStreamException { - request.read(message, true); - assertEquals(METHOD_POST, request.getMethod()); + private void testForEmptyBody(InputStream is) throws IOException, EndOfStreamException { + request.read(is, true); assertInstanceOf(EmptyBody.class, request.getBody()); } @@ -332,6 +363,28 @@ void isXML() throws URISyntaxException { assertTrue(post("/foo").header("Content-Type", "text/xml; charset=utf-8").build().isXML()); } + @Nested + class Http10 { + + @Test + void noBody() throws Exception { + request.read(inHttp10NoBody, true); + + // We do not know if there is a body or not. Therefore prepare just in case + assertInstanceOf(Body.class, request.getBody()); + assertEquals(0, request.getBody().getLength()); + } + + @Test + void body() throws Exception { + request.read(inHttp10Body,true); + assertInstanceOf(Body.class, request.getBody()); + assertEquals(3, request.getBody().getLength()); + assertEquals("Foo", request.getBodyAsStringDecoded()); + } + + } + private AbstractBody readMessageAndGetBody() throws IOException, EndOfStreamException { request.read(inPost, true); assertFalse(request.getBody().isRead()); @@ -340,8 +393,8 @@ private AbstractBody readMessageAndGetBody() throws IOException, EndOfStreamExce public Request create(String method, String uri, String protocol, Header header, InputStream in) throws IOException { var r = new Request(); - r.method = method; - r.uri = uri; + r.setMethod(method); + r.setUri(uri); if (!protocol.startsWith("HTTP/")) throw new RuntimeException("Unknown protocol '" + protocol + "'"); r.version = protocol.substring(5);