http-client-branch: add offline testing example http-client-branch
authorchegar
Thu, 18 Jan 2018 11:26:39 +0000
branchhttp-client-branch
changeset 56023 fa36a61f4cbf
parent 56020 ae3a51bc5b9f
child 56024 de352132c7e8
http-client-branch: add offline testing example
test/jdk/java/net/httpclient/offline/DelegatingHttpClient.java
test/jdk/java/net/httpclient/offline/FixedHttpHeaders.java
test/jdk/java/net/httpclient/offline/FixedHttpResponse.java
test/jdk/java/net/httpclient/offline/FixedResponseHttpClient.java
test/jdk/java/net/httpclient/offline/OfflineTesting.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/offline/DelegatingHttpClient.java	Thu Jan 18 11:26:39 2018 +0000
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.ProxySelector;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+
+/**
+ * An HttpClient that delegates all its operations to the given client.
+ */
+public class DelegatingHttpClient extends HttpClient {
+
+    private final HttpClient client;
+
+    public DelegatingHttpClient(HttpClient client) {
+        this.client = client;
+    }
+
+    @Override
+    public Optional<CookieHandler> cookieHandler() {
+        return client.cookieHandler();
+    }
+
+    @Override
+    public Redirect followRedirects() {
+        return client.followRedirects();
+    }
+
+    @Override
+    public Optional<ProxySelector> proxy() {
+        return client.proxy();
+    }
+
+    @Override
+    public SSLContext sslContext() {
+        return client.sslContext();
+    }
+
+    @Override
+    public SSLParameters sslParameters() {
+        return client.sslParameters();
+    }
+
+    @Override
+    public Optional<Authenticator> authenticator() {
+        return client.authenticator();
+    }
+
+    @Override
+    public Version version() {
+        return client.version();
+    }
+
+    @Override
+    public Optional<Executor> executor() {
+        return client.executor();
+    }
+
+    @Override
+    public <T> HttpResponse<T> send(HttpRequest request,
+                                    HttpResponse.BodyHandler<T> responseBodyHandler)
+            throws IOException, InterruptedException {
+        return client.send(request, responseBodyHandler);
+    }
+
+    @Override
+    public <T> CompletableFuture<HttpResponse<T>>
+    sendAsync(HttpRequest request,
+              HttpResponse.BodyHandler<T> responseBodyHandler) {
+        return client.sendAsync(request, responseBodyHandler);
+    }
+
+    @Override
+    public <T> CompletableFuture<HttpResponse<T>>
+    sendAsync(HttpRequest request,
+              HttpResponse.BodyHandler<T> responseBodyHandler,
+              HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
+        return client.sendAsync(request, responseBodyHandler, pushPromiseHandler);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/offline/FixedHttpHeaders.java	Thu Jan 18 11:26:39 2018 +0000
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import jdk.incubator.http.HttpHeaders;
+
+/**
+ * An HttpHeaders consisting of the given name value pairs.
+ */
+public class FixedHttpHeaders extends HttpHeaders {
+
+    private final Map<String, List<String>> map =
+            new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+    @Override
+    public Map<String, List<String>> map() {
+        return map;
+    }
+
+    /**
+     * Creates an HttpHeaders of the given name value pairs.
+     */
+    public static HttpHeaders of(String... params) {
+        Objects.requireNonNull(params);
+        if ((params.length & 0x1) != 0)
+            throw new IllegalArgumentException("odd number of params");
+        FixedHttpHeaders headers = new FixedHttpHeaders();
+        for (int i = 0; i < params.length; i += 2) {
+            String name = params[i];
+            String value = params[i + 1];
+            headers.map.computeIfAbsent(name, k -> new ArrayList<>(1))
+                       .add(value);
+        }
+        return headers;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/offline/FixedHttpResponse.java	Thu Jan 18 11:26:39 2018 +0000
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javax.net.ssl.SSLParameters;
+import java.net.URI;
+import java.util.Optional;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+
+/**
+ * An HttpResponse consisting of the given state.
+ */
+public class FixedHttpResponse<T> extends HttpResponse<T> {
+
+    private final int statusCode;
+    private final HttpRequest request;
+    private final HttpHeaders headers;
+    private final T body;
+    private final SSLParameters sslParameters;
+    private final URI uri;
+    private final HttpClient.Version version;
+
+    public FixedHttpResponse(int statusCode,
+                             HttpRequest request,
+                             HttpHeaders headers,
+                             T body,
+                             SSLParameters sslParameters,
+                             URI uri,
+                             HttpClient.Version version) {
+        this.statusCode = statusCode;
+        this.request = request;
+        this.headers = headers;
+        this.body = body;
+        this.sslParameters = sslParameters;
+        this.uri = uri;
+        this.version = version;
+    }
+
+    @Override
+    public int statusCode() {
+        return statusCode;
+    }
+
+    @Override
+    public HttpRequest request() {
+        return request;
+    }
+
+    @Override
+    public Optional<HttpResponse<T>> previousResponse() {
+        return Optional.empty();
+    }
+
+    @Override
+    public HttpHeaders headers() {
+        return headers;
+    }
+
+    @Override
+    public T body() {
+        return body;
+    }
+
+    @Override
+    public SSLParameters sslParameters() {
+        return sslParameters;
+    }
+
+    @Override
+    public URI uri() {
+        return uri;
+    }
+
+    @Override
+    public HttpClient.Version version() {
+        return version;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        return sb.append(super.toString()).append(" [ ")
+                .append("status code: ").append(statusCode)
+                .append(", request: ").append(request)
+                .append(", headers: ").append(headers)
+                .append(" ]")
+                .toString();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/offline/FixedResponseHttpClient.java	Thu Jan 18 11:26:39 2018 +0000
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import java.util.concurrent.SubmissionPublisher;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpHeaders;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.incubator.http.HttpResponse.BodyHandler;
+import jdk.incubator.http.HttpResponse.BodySubscriber;
+import jdk.incubator.http.HttpResponse.PushPromiseHandler;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.ByteBuffer.wrap;
+
+/**
+ * An HttpClient that returns a given fixed response.
+ * Suitable for testing where network connections are to be avoided.
+ */
+public class FixedResponseHttpClient extends DelegatingHttpClient {
+    private final ByteBuffer responseBodyBytes;
+    private final int responseStatusCode;
+    private final HttpHeaders responseHeaders;
+
+    private FixedResponseHttpClient(HttpClient.Builder builder,
+                                    int responseStatusCode,
+                                    HttpHeaders responseHeaders,
+                                    ByteBuffer responseBodyBytes) {
+        super(builder.build());
+        this.responseStatusCode = responseStatusCode;
+        this.responseHeaders = responseHeaders;
+        this.responseBodyBytes = responseBodyBytes;
+    }
+
+    /**
+     * Creates a new HttpClient that returns a fixed response,
+     * constructed from the given values, for every request sent.
+     */
+    public static HttpClient createClientFrom(HttpClient.Builder builder,
+                                              int responseStatusCode,
+                                              HttpHeaders responseHeaders,
+                                              String responseBody) {
+        return new FixedResponseHttpClient(builder,
+                                           responseStatusCode,
+                                           responseHeaders,
+                                           wrap(responseBody.getBytes(UTF_8)));
+    }
+
+    /**
+     * Creates a new HttpClient that returns a fixed response,
+     * constructed from the given values, for every request sent.
+     */
+    public static HttpClient createClientFrom(HttpClient.Builder builder,
+                                              int responseStatusCode,
+                                              HttpHeaders responseHeaders,
+                                              Path path) {
+        try {
+            return new FixedResponseHttpClient(builder,
+                                               responseStatusCode,
+                                               responseHeaders,
+                                               wrap(Files.readAllBytes(path)));
+        } catch (IOException ioe) {
+            throw new UncheckedIOException(ioe);
+        }
+    }
+
+    /**
+     * Creates a new HttpClient that returns a fixed response,
+     * constructed from the given values, for every request sent.
+     */
+    public static HttpClient createClientFrom(HttpClient.Builder builder,
+                                              int responseStatusCode,
+                                              HttpHeaders responseHeaders,
+                                              byte[] responseBody) {
+        return new FixedResponseHttpClient(builder,
+                responseStatusCode,
+                responseHeaders,
+                wrap(responseBody));
+    }
+
+    private static final ByteBuffer ECHO_SENTINAL = ByteBuffer.wrap(new byte[] {});
+
+    /**
+     * Creates a new HttpClient that returns a fixed response,
+     * constructed from the given values, for every request sent.
+     */
+    public static HttpClient createEchoClient(HttpClient.Builder builder,
+                                              int responseStatusCode,
+                                              HttpHeaders responseHeaders) {
+        return new FixedResponseHttpClient(builder,
+                                           responseStatusCode,
+                                           responseHeaders,
+                                           ECHO_SENTINAL);
+    }
+
+    @Override
+    public <T> CompletableFuture<HttpResponse<T>>
+    sendAsync(HttpRequest request, BodyHandler<T> responseBodyHandler) {
+        return sendAsync(request, responseBodyHandler, null);
+    }
+
+    @Override
+    public <T> HttpResponse<T> send(HttpRequest request,
+                                    BodyHandler<T> responseBodyHandler)
+            throws IOException, InterruptedException {
+        return sendAsync(request, responseBodyHandler).join();  // unwrap exceptions if needed
+    }
+
+    @Override
+    public <T> CompletableFuture<HttpResponse<T>>
+    sendAsync(HttpRequest request,
+              BodyHandler<T> responseBodyHandler,
+              PushPromiseHandler<T> pushPromiseHandler) {
+        List<ByteBuffer> responseBody = List.of(responseBodyBytes.duplicate());
+
+        // Push promises can be mocked too, if needed
+
+        Optional<HttpRequest.BodyPublisher> obp = request.bodyPublisher();
+        if (obp.isPresent()) {
+            ConsumingSubscriber subscriber = new ConsumingSubscriber();
+            obp.get().subscribe(subscriber);
+            if (responseBodyBytes == ECHO_SENTINAL) {
+                responseBody = subscriber.buffers;
+            }
+        }
+
+        BodySubscriber<T> bodySubscriber =
+                responseBodyHandler.apply(responseStatusCode, responseHeaders);
+        SubmissionPublisher<List<ByteBuffer>> publisher = new SubmissionPublisher<>();
+        publisher.subscribe(bodySubscriber);
+        publisher.submit(responseBody);
+        publisher.close();
+
+        CompletableFuture<HttpResponse<T>> cf = new CompletableFuture<>();
+        bodySubscriber.getBody().whenComplete((body, throwable) -> {
+                    if (body != null)
+                        cf.complete(new FixedHttpResponse<>(
+                                responseStatusCode,
+                                request,
+                                responseHeaders,
+                                body,
+                                this.sslParameters(),
+                                request.uri(),
+                                request.version().orElse(Version.HTTP_2)));
+                    else
+                        cf.completeExceptionally(throwable);
+                }
+        );
+
+        return cf;
+    }
+
+    /**
+     * A Subscriber that demands and consumes all the Publishers data,
+     * after which makes it directly available.
+     */
+    private static class ConsumingSubscriber implements Flow.Subscriber<ByteBuffer> {
+        final List<ByteBuffer> buffers = Collections.synchronizedList(new ArrayList<>());
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            subscription.request(Long.MAX_VALUE);
+        }
+
+        @Override public void onNext(ByteBuffer item) {
+            buffers.add(item.duplicate());
+        }
+
+        @Override public void onError(Throwable throwable) { assert false : "Unexpected"; }
+
+        @Override public void onComplete() { /* do nothing */ }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/offline/OfflineTesting.java	Thu Jan 18 11:26:39 2018 +0000
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Demonstrates how to achieve testing without network connections
+ * @build FixedHttpHeaders DelegatingHttpClient FixedHttpResponse FixedResponseHttpClient
+ * @run testng/othervm OfflineTesting
+ */
+
+import java.io.IOException;
+import java.net.URI;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import org.testng.annotations.Test;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asByteArray;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+public class OfflineTesting {
+
+    private static HttpClient getClient() {
+        // be sure to return the appropriate client when testing
+        //return HttpClient.newHttpClient();
+        return FixedResponseHttpClient.createClientFrom(
+                HttpClient.newBuilder(),
+                200,
+                FixedHttpHeaders.of("Server",  "nginx",
+                                    "Content-Type", "text/html"),
+                "A response message");
+    }
+
+    @Test
+    public void testResponseAsString() {
+        HttpClient client = getClient();
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create("http://openjdk.java.net/"))
+                .build();
+
+        client.sendAsync(request, asString())
+                .thenAccept(response -> {
+                    System.out.println("response: " + response);
+                    assertEquals(response.statusCode(), 200);
+                    assertTrue(response.headers().firstValue("Server").isPresent());
+                    assertEquals(response.body(), "A response message"); } )
+                .join();
+    }
+
+    @Test
+    public void testResponseAsByteArray() {
+        HttpClient client = getClient();
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create("http://openjdk.java.net/"))
+                .build();
+
+        client.sendAsync(request, asByteArray())
+                .thenAccept(response -> {
+                    System.out.println("response: " + response);
+                    assertEquals(response.statusCode(), 200);
+                    assertTrue(response.headers().firstValue("Content-Type").isPresent());
+                    assertEquals(response.body(), "A response message".getBytes(UTF_8)); } )
+                .join();
+    }
+
+    @Test
+    public void testFileNotFound() {
+        //HttpClient client = HttpClient.newHttpClient();
+        HttpClient client = FixedResponseHttpClient.createClientFrom(
+                HttpClient.newBuilder(),
+                404,
+                FixedHttpHeaders.of("Connection",  "keep-alive",
+                                    "Content-Length", "162",
+                                    "Content-Type", "text/html",
+                                    "Date", "Mon, 15 Jan 2018 15:01:16 GMT",
+                                    "Server", "nginx"),
+                "<html>\n" +
+                "<head><title>404 Not Found</title></head>\n" +
+                "<body bgcolor=\"white\">\n" +
+                "<center><h1>404 Not Found</h1></center>\n" +
+                "<hr><center>nginx</center>\n" +
+                "</body>\n" +
+                "</html>");
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create("http://openjdk.java.net/notFound"))
+                .build();
+
+        client.sendAsync(request, asString())
+                .thenAccept(response -> {
+                    assertEquals(response.statusCode(), 404);
+                    response.headers().firstValue("Content-Type")
+                            .ifPresentOrElse(type -> assertEquals(type, "text/html"),
+                                             () -> fail("Content-Type not present"));
+                    assertTrue(response.body().contains("404 Not Found")); } )
+                .join();
+    }
+
+    @Test
+    public void testEcho() {
+        HttpClient client = FixedResponseHttpClient.createEchoClient(
+                HttpClient.newBuilder(),
+                200,
+                FixedHttpHeaders.of("Connection",  "keep-alive"));
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create("http://openjdk.java.net/echo"))
+                .POST(fromString("Hello World"))
+                .build();
+
+        client.sendAsync(request, asString())
+                .thenAccept(response -> {
+                    System.out.println("response: " + response);
+                    assertEquals(response.statusCode(), 200);
+                    assertEquals(response.body(), "Hello World"); } )
+                .join();
+    }
+
+    @Test
+    public void testEchoBlocking() throws IOException, InterruptedException {
+        HttpClient client = FixedResponseHttpClient.createEchoClient(
+                HttpClient.newBuilder(),
+                200,
+                FixedHttpHeaders.of("Connection",  "keep-alive"));
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create("http://openjdk.java.net/echo"))
+                .POST(fromString("Hello chegar!!"))
+                .build();
+
+        HttpResponse<String> response = client.send(request, asString());
+        System.out.println("response: " + response);
+        assertEquals(response.statusCode(), 200);
+        assertEquals(response.body(), "Hello chegar!!");
+    }
+}