/**
* The MIT License
* Copyright (c) 2014 JMXTrans Team
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jmxtrans.core.output.support;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.jmxtrans.core.log.Logger;
import org.jmxtrans.core.results.QueryResult;
import org.jmxtrans.utils.appinfo.AppInfo;
import org.jmxtrans.utils.io.NullOutputStream;
import static java.lang.String.format;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static javax.xml.bind.DatatypeConverter.printBase64Binary;
import static org.jmxtrans.core.log.LoggerFactory.getLogger;
import static org.jmxtrans.utils.io.Charsets.US_ASCII;
import static org.jmxtrans.utils.io.IoUtils.copy;
public class HttpOutputWriter<T extends OutputStreamBasedOutputWriter> implements BatchedOutputWriter {
@Nonnull private final Logger logger = getLogger(getClass().getName());
@Nonnull private final ThreadLocal<HttpURLConnection> connection = new ThreadLocal<>();
@Nonnull final private URL url;
private final int timeoutInMillis;
@Nullable final private Proxy proxy;
@Nullable final private String contentType;
@Nullable final private String basicAuthentication;
@Nonnull final private AppInfo<?> appInfo;
@Nonnull final private T target;
private HttpOutputWriter(
@Nonnull URL url,
int timeoutInMillis,
@Nullable Proxy proxy,
@Nullable String contentType,
@Nullable String basicAuthentication,
@Nonnull AppInfo<?> appInfo,
@Nonnull T target) {
this.url = url;
this.timeoutInMillis = timeoutInMillis;
this.proxy = proxy;
this.contentType = contentType;
this.basicAuthentication = basicAuthentication;
this.appInfo = appInfo;
this.target = target;
}
@Override
public void beforeBatch() throws IOException {
HttpURLConnection urlConnection = openConnection();
configureConnection(urlConnection);
connection.set(urlConnection);
target.beforeBatch(getURLConnection().getOutputStream());
}
@Override
public int write(@Nonnull QueryResult result) throws IOException {
HttpURLConnection urlConnection = getURLConnection();
int count = target.write(urlConnection.getOutputStream(), result);
return count;
}
private HttpURLConnection getURLConnection() {
HttpURLConnection urlConnection = connection.get();
if (urlConnection == null) throw new IllegalStateException("Connection has not been initialized");
return urlConnection;
}
@Override
public int afterBatch() throws IOException {
HttpURLConnection urlConnection = getURLConnection();
try {
return target.afterBatch(urlConnection.getOutputStream());
} finally {
if (urlConnection.getResponseCode() != HTTP_OK) {
throw new IOException("Error connecting to server, response code is not OK but " + urlConnection.getResponseCode());
}
try {
disposeOfConnection(connection.get());
} finally {
connection.remove();
}
}
}
@Nonnull
private HttpURLConnection openConnection() throws IOException {
if (proxy == null) return (HttpURLConnection) url.openConnection();
return (HttpURLConnection) url.openConnection(proxy);
}
private void configureConnection(@Nonnull HttpURLConnection urlConnection) throws ProtocolException {
urlConnection.setRequestMethod("POST");
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
urlConnection.setReadTimeout(timeoutInMillis);
if (contentType != null) {
urlConnection.setRequestProperty("content-type", contentType);
}
if (basicAuthentication != null) {
urlConnection.setRequestProperty("Authorization", "Basic " + basicAuthentication);
}
urlConnection.setRequestProperty("User-Agent", appInfo.getUserAgent());
}
private void disposeOfConnection(@Nullable HttpURLConnection urlConnection) {
if (urlConnection != null) {
consumeInputStream(urlConnection);
consumeErrorStream(urlConnection);
}
}
private void consumeInputStream(@Nonnull HttpURLConnection urlConnection) {
try (InputStream in = urlConnection.getInputStream()) {
if (in != null) copy(in, new NullOutputStream());
} catch (FileNotFoundException ignore) {
} catch (IOException e) {
logger.error("Could not consume input stream", e);
}
}
private void consumeErrorStream(@Nonnull HttpURLConnection urlConnection) {
try (InputStream in = urlConnection.getErrorStream()) {
if (in != null) copy(in, new NullOutputStream());
} catch (IOException e) {
logger.error("Could not consume error stream", e);
}
}
@Nonnull
public static <T extends OutputStreamBasedOutputWriter> Builder<T> builder(
@Nonnull URL url, @Nonnull AppInfo<?> appInfo, @Nonnull T target) {
return new Builder(url, appInfo, target);
}
public static final class Builder<T extends OutputStreamBasedOutputWriter> {
@Nonnull private final List<String> VALID_PROTOCOLS = asList("HTTP", "HTTPS");
@Nonnull private final URL url;
@Nullable private final AppInfo<?> appInfo;
@Nonnull private final T target;
private int timeoutInMillis = 1000;
@Nullable private Proxy proxy;
@Nullable private String contentType;
@Nullable private String basicAuthentication;
public Builder(URL url, AppInfo<?> appInfo, T target) {
this.url = validateHttp(url);
this.appInfo = appInfo;
this.target = target;
}
@Nonnull
public Builder<T> withAuthentication(@Nonnull String username, @Nonnull String password) {
basicAuthentication = printBase64Binary((username + ":" + password).getBytes(US_ASCII));
return this;
}
@Nonnull
public Builder<T> withTimeout(int value, @Nonnull TimeUnit unit) {
timeoutInMillis = (int) MILLISECONDS.convert(value, unit);
return this;
}
@Nonnull
public Builder<T> withProxy(@Nonnull Proxy proxy) {
this.proxy = proxy;
return this;
}
@Nonnull
public Builder<T> withContentType(@Nonnull String contentType) {
this.contentType = contentType;
return this;
}
@Nonnull
public HttpOutputWriter<T> build() {
return new HttpOutputWriter<>(url, timeoutInMillis, proxy, contentType, basicAuthentication, appInfo, target);
}
@Nonnull
private URL validateHttp(@Nonnull URL url) {
if (!VALID_PROTOCOLS.contains(url.getProtocol().toUpperCase())) {
throw new IllegalArgumentException(format("URL needs to be HTTP or HTTPS [%s]", url));
}
return url;
}
}
}