--- /dev/null 2016-04-22 22:42:07.067228853 +0100 +++ new/src/java.httpclient/share/classes/java/net/http/WebSocketBuilderImpl.java 2016-04-25 23:11:11.749374081 +0100 @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2015, 2016, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package java.net.http; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +final class WebSocketBuilderImpl implements WebSocket.Builder { + + private static final Set FORBIDDEN_HEADERS_LOWER_CASED = Set.of( + "connection", "upgrade", "sec-websocket-accept", "sec-websocket-extensions", + "sec-websocket-key", "sec-websocket-protocol", "sec-websocket-version"); + + private final URI uri; + private final HttpClient client; + private final LinkedHashMap> headers = new LinkedHashMap<>(); + private final WebSocket.Listener listener; + private Collection subprotocols = Collections.emptyList(); + private long timeout; + private TimeUnit timeUnit; + + WebSocketBuilderImpl(URI uri, HttpClient client, WebSocket.Listener listener) { + requireNonNull(uri, "uri"); + requireNonNull(client, "client"); + requireNonNull(listener, "listener"); + checkUri(uri); + this.uri = uri; + this.listener = listener; + this.client = client; + } + + @Override + public WebSocket.Builder header(String name, String value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + // TODO: I don't like the fact the knowledge about + // case-insensitivity, order and name->list(value) structure, etc. + // sits is in the builder + if (FORBIDDEN_HEADERS_LOWER_CASED.contains(name.toLowerCase(Locale.ROOT))) { + throw new IllegalArgumentException( + format("Header '%s' is used in the WebSocket Protocol", name)); + } + List values = headers.computeIfAbsent(name, n -> new LinkedList<>()); + values.add(value); + return this; + } + + @Override + public WebSocket.Builder subprotocols(String mostPreferred, String... lesserPreferred) { + requireNonNull(mostPreferred, "mostPreferred"); + requireNonNull(lesserPreferred, "lesserPreferred"); + this.subprotocols = checkAndReturnSubprotocols(mostPreferred, lesserPreferred); + return this; + } + + @Override + public WebSocket.Builder connectTimeout(long timeout, TimeUnit unit) { + if (timeout < 0) { + throw new IllegalArgumentException("Negative timeout: " + timeout); + } + requireNonNull(unit, "unit"); + this.timeout = timeout; + this.timeUnit = unit; + return this; + } + + @Override + public CompletableFuture buildAsync() { + return WebSocketImpl.newInstanceAsync(this); + } + + private static void checkUri(URI uri) { + String s = uri.getScheme(); + if (!("ws".equalsIgnoreCase(s) || "wss".equalsIgnoreCase(s))) { + throw new IllegalArgumentException + ("URI scheme not ws or wss (RFC 6455 3.): " + s); + } + String fragment = uri.getFragment(); + if (fragment != null) { + throw new IllegalArgumentException(format + ("Fragment not allowed in a WebSocket URI (RFC 6455 3.): '%s'", + fragment)); + } + } + + URI getUri() { return uri; } + + HttpClient getClient() { return client; } + + Map> getHeaders() { + LinkedHashMap> copy = new LinkedHashMap<>(headers.size()); + headers.forEach((name, values) -> copy.put(name, new LinkedList<>(values))); + return copy; + } + + WebSocket.Listener getListener() { return listener; } + + Collection getSubprotocols() { + return new ArrayList<>(subprotocols); + } + + long getTimeout() { return timeout; } + + TimeUnit getTimeUnit() { return timeUnit; } + + private static Collection checkAndReturnSubprotocols(String mostPreferred, + String... lesserPreferred) { + checkSubprotocolSyntax(mostPreferred, "mostPreferred"); + LinkedHashSet subprotocols = new LinkedHashSet<>(1 + lesserPreferred.length); + subprotocols.add(mostPreferred); + for (int i = 0; i < lesserPreferred.length; i++) { + String p = lesserPreferred[i]; + String location = format("lesserPreferred[%s]", i); + requireNonNull(p, location); + checkSubprotocolSyntax(p, location); + if (!subprotocols.add(p)) { + throw new IllegalArgumentException(format( + "Duplicate subprotocols (RFC 6455 4.1.): '%s'", p)); + } + } + return subprotocols; + } + + private static void checkSubprotocolSyntax(String subprotocol, String location) { + if (subprotocol.isEmpty()) { + throw new IllegalArgumentException + ("Subprotocol name is empty (RFC 6455 4.1.): " + location); + } + if (!subprotocol.chars().allMatch(c -> 0x21 <= c && c <= 0x7e)) { + throw new IllegalArgumentException + ("Subprotocol name contains illegal characters (RFC 6455 4.1.): " + + location); + } + } +}