/**
*
* Copyright (c) 2006-2017, Speedment, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); You may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.speedment.common.rest;
import static com.speedment.common.rest.Option.Type.HEADER;
import static com.speedment.common.rest.Option.Type.PARAM;
import static com.speedment.common.rest.Rest.encode;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Iterator;
import static java.util.Objects.requireNonNull;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.stream.Collectors.joining;
import java.util.stream.Stream;
/**
* Default implementation of the {@link Rest}-interface.
*
* @author Emil Forslund
* @since 1.0.0
*/
class RestImpl implements Rest {
private final Protocol protocol;
private final String host;
private final int port;
private final String username;
private final String password;
RestImpl(Protocol protocol, String host, int port, String username, String password) {
this.protocol = requireNonNull(protocol);
this.host = requireNonNull(host);
this.port = port;
this.username = username; // Can be null
this.password = password; // Can be null
}
@Override
public CompletableFuture<Response> get(String path, Option... option) {
return send(Method.GET, path, option);
}
@Override
public CompletableFuture<Response> post(String path, Option... option) {
return send(Method.POST, path, option);
}
@Override
public CompletableFuture<Response> delete(String path, Option... option) {
return send(Method.DELETE, path, option);
}
@Override
public CompletableFuture<Response> put(String path, Option... option) {
return send(Method.PUT, path, option);
}
@Override
public CompletableFuture<Response> options(String path, Option... option) {
return send(Method.OPTIONS, path, option);
}
@Override
public CompletableFuture<Response> get(String path, Iterator<String> uploader, Option... option) {
return send(Method.GET, path, option, uploader);
}
@Override
public CompletableFuture<Response> post(String path, Iterator<String> uploader, Option... option) {
return send(Method.POST, path, option, uploader);
}
@Override
public CompletableFuture<Response> delete(String path, Iterator<String> uploader, Option... option) {
return send(Method.DELETE, path, option, uploader);
}
@Override
public CompletableFuture<Response> put(String path, Iterator<String> uploader, Option... option) {
return send(Method.PUT, path, option, uploader);
}
@Override
public CompletableFuture<Response> options(String path, Iterator<String> uploader, Option... option) {
return send(Method.OPTIONS, path, option, uploader);
}
@Override
public CompletableFuture<Response> get(String path, InputStream body, Option... option) {
return send(Method.GET, path, option, stream(body));
}
@Override
public CompletableFuture<Response> post(String path, InputStream body, Option... option) {
return send(Method.POST, path, option, stream(body));
}
@Override
public CompletableFuture<Response> delete(String path, InputStream body, Option... option) {
return send(Method.DELETE, path, option, stream(body));
}
@Override
public CompletableFuture<Response> put(String path, InputStream body, Option... option) {
return send(Method.PUT, path, option, stream(body));
}
@Override
public CompletableFuture<Response> options(String path, InputStream body, Option... option) {
return send(Method.OPTIONS, path, option, stream(body));
}
@Override
public CompletableFuture<Response> get(String path, String data, Option... option) {
return send(Method.GET, path, option, new SingletonIterator<>(data));
}
@Override
public CompletableFuture<Response> post(String path, String data, Option... option) {
return send(Method.POST, path, option, new SingletonIterator<>(data));
}
@Override
public CompletableFuture<Response> delete(String path, String data, Option... option) {
return send(Method.DELETE, path, option, new SingletonIterator<>(data));
}
@Override
public CompletableFuture<Response> put(String path, String data, Option... option) {
return send(Method.PUT, path, option, new SingletonIterator<>(data));
}
@Override
public CompletableFuture<Response> options(String path, String data, Option... option) {
return send(Method.OPTIONS, path, option, new SingletonIterator<>(data));
}
protected String getProtocol() {
switch (protocol) {
case HTTP : return "http";
case HTTPS : return "https";
default : throw new UnsupportedOperationException(
"Unknown enum constant '" + protocol + "'."
);
}
}
protected String getHost() {
return host;
}
protected int getPort() {
return port;
}
protected final URL getUrl(String path, Param... param) {
try {
final StringBuilder url = new StringBuilder()
.append(getProtocol())
.append("://")
.append(host);
if (port > 0) {
url.append(":").append(port);
}
url.append("/").append(path);
if (param.length > 0) {
url.append(
Stream.of(param)
.map(p ->
encode(p.getKey()) +
"=" +
encode(p.getValue())
)
.collect(joining("&", "?", ""))
);
}
return new URL(url.toString());
} catch (final MalformedURLException ex) {
throw new RuntimeException("Error building URL.", ex);
}
}
private final static int BUFFER_SIZE = 1024;
private StreamConsumer stream(InputStream in) {
return out -> {
final byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
};
}
private CompletableFuture<Response> send(Method method, String path, Option[] options) {
return send(method, path, options, NO_ITERATOR);
}
private CompletableFuture<Response> send(Method method, String path, Option[] options, Iterator<String> iterator) {
if (iterator == NO_ITERATOR) {
return send(method, path, options, StreamConsumer.IGNORE);
} else {
return send(method, path, options, out -> {
while (iterator.hasNext()) {
out.write(iterator.next().getBytes(StandardCharsets.UTF_8));
}
});
}
}
private CompletableFuture<Response> send(Method method, String path, Option[] options, StreamConsumer outStreamConsumer) {
return CompletableFuture.supplyAsync(() -> {
final Param[] params = Stream.of(options).filter(o -> o.getType() == PARAM).toArray(Param[]::new);
final Header[] headers = Stream.of(options).filter(o -> o.getType() == HEADER).toArray(Header[]::new);
final URL url = getUrl(path, params);
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
switch (method) {
case POST : conn.setRequestMethod("POST"); break;
case GET : conn.setRequestMethod("GET"); break;
case DELETE : conn.setRequestMethod("DELETE"); break;
case OPTIONS : conn.setRequestMethod("OPTIONS"); break;
case PUT : conn.setRequestMethod("PUT"); break;
default : throw new UnsupportedOperationException(
"Unknown enum constant '" + method + "'."
);
}
if (username != null && password != null) {
final byte[] authentication = (username + ":" + password).getBytes();
final String encoding = Base64.getEncoder().encodeToString(authentication);
conn.setRequestProperty("Authorization", "Basic " + encoding);
}
for (final Header header : headers) {
conn.setRequestProperty(
header.getKey(),
header.getValue());
}
conn.setUseCaches(false);
conn.setAllowUserInteraction(false);
final boolean doOutput = outStreamConsumer != StreamConsumer.IGNORE;
conn.setDoOutput(doOutput);
conn.connect();
if (doOutput) {
try (final OutputStream out = conn.getOutputStream()) {
outStreamConsumer.writeTo(out);
out.flush();
}
}
int status = getResponseCodeFrom(conn);
final String text;
try (final BufferedReader rd = new BufferedReader(
new InputStreamReader(status >= 400
? conn.getErrorStream()
: conn.getInputStream()))) {
final StringBuilder sb = new StringBuilder();
String line;
while ((line = rd.readLine()) != null) {
sb.append(line);
}
text = sb.toString();
}
return new Response(status, text, conn.getHeaderFields());
} catch (final IOException ex) {
throw new RuntimeException("Could not send " + method.name() + "-command.", ex);
} finally {
if (conn != null) {
conn.disconnect();
}
}
});
}
private static int getResponseCodeFrom(HttpURLConnection conn) throws IOException {
try {
return conn.getResponseCode();
} catch (final FileNotFoundException ex) {
return 404;
}
}
private final static Iterator<String> NO_ITERATOR = new Iterator<String>() {
@Override
public boolean hasNext() {
return false;
}
@Override
public String next() {
throw new IllegalStateException(
"This method should never be called."
);
}
};
private final static class SingletonIterator<T> implements Iterator<T> {
private final AtomicBoolean first = new AtomicBoolean(true);
private final T instance;
private SingletonIterator(T instance) {
this.instance = instance;
}
@Override
public boolean hasNext() {
return first.compareAndSet(true, false);
}
@Override
public T next() {
return instance;
}
}
@FunctionalInterface
private interface StreamConsumer {
StreamConsumer IGNORE = o -> {};
void writeTo(OutputStream out) throws IOException;
}
}