/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.nifi.remote.util;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.HANDSHAKE_PROPERTY_BATCH_COUNT;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.HANDSHAKE_PROPERTY_BATCH_DURATION;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.HANDSHAKE_PROPERTY_BATCH_SIZE;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.HANDSHAKE_PROPERTY_REQUEST_EXPIRATION;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.HANDSHAKE_PROPERTY_USE_COMPRESSION;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_HEADER_NAME;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_NAME;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_VALUE;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpInetConnection;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.nio.ContentEncoder;
import org.apache.http.nio.IOControl;
import org.apache.http.nio.conn.ManagedNHttpClientConnection;
import org.apache.http.nio.protocol.BasicAsyncResponseConsumer;
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.http.util.EntityUtils;
import org.apache.nifi.events.EventReporter;
import org.apache.nifi.remote.Peer;
import org.apache.nifi.remote.TransferDirection;
import org.apache.nifi.remote.client.http.TransportProtocolVersionNegotiator;
import org.apache.nifi.remote.exception.HandshakeException;
import org.apache.nifi.remote.exception.PortNotRunningException;
import org.apache.nifi.remote.exception.ProtocolException;
import org.apache.nifi.remote.exception.UnknownPortException;
import org.apache.nifi.remote.io.http.HttpCommunicationsSession;
import org.apache.nifi.remote.io.http.HttpInput;
import org.apache.nifi.remote.io.http.HttpOutput;
import org.apache.nifi.remote.protocol.CommunicationsSession;
import org.apache.nifi.remote.protocol.ResponseCode;
import org.apache.nifi.remote.protocol.http.HttpHeaders;
import org.apache.nifi.remote.protocol.http.HttpProxy;
import org.apache.nifi.reporting.Severity;
import org.apache.nifi.security.util.CertificateUtils;
import org.apache.nifi.stream.io.ByteArrayInputStream;
import org.apache.nifi.stream.io.ByteArrayOutputStream;
import org.apache.nifi.stream.io.StreamUtils;
import org.apache.nifi.web.api.dto.ControllerDTO;
import org.apache.nifi.web.api.dto.remote.PeerDTO;
import org.apache.nifi.web.api.entity.ControllerEntity;
import org.apache.nifi.web.api.entity.PeersEntity;
import org.apache.nifi.web.api.entity.TransactionResultEntity;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SiteToSiteRestApiClient implements Closeable {
private static final String EVENT_CATEGORY = "Site-to-Site";
private static final int DATA_PACKET_CHANNEL_READ_BUFFER_SIZE = 16384;
private static final int RESPONSE_CODE_OK = 200;
private static final int RESPONSE_CODE_CREATED = 201;
private static final int RESPONSE_CODE_ACCEPTED = 202;
private static final int RESPONSE_CODE_BAD_REQUEST = 400;
private static final int RESPONSE_CODE_FORBIDDEN = 403;
private static final int RESPONSE_CODE_NOT_FOUND = 404;
private static final Logger logger = LoggerFactory.getLogger(SiteToSiteRestApiClient.class);
private String baseUrl;
protected final SSLContext sslContext;
protected final HttpProxy proxy;
private final AtomicBoolean proxyAuthRequiresResend = new AtomicBoolean(false);
private final EventReporter eventReporter;
private RequestConfig requestConfig;
private CredentialsProvider credentialsProvider;
private CloseableHttpClient httpClient;
private CloseableHttpAsyncClient httpAsyncClient;
private boolean compress = false;
private InetAddress localAddress = null;
private long requestExpirationMillis = 0;
private int serverTransactionTtl = 0;
private int batchCount = 0;
private long batchSize = 0;
private long batchDurationMillis = 0;
private TransportProtocolVersionNegotiator transportProtocolVersionNegotiator = new TransportProtocolVersionNegotiator(1);
private String trustedPeerDn;
private final ScheduledExecutorService ttlExtendTaskExecutor;
private ScheduledFuture<?> ttlExtendingFuture;
private SiteToSiteRestApiClient extendingApiClient;
private int connectTimeoutMillis;
private int readTimeoutMillis;
private static final Pattern HTTP_ABS_URL = Pattern.compile("^https?://.+$");
private Future<HttpResponse> postResult;
private CountDownLatch transferDataLatch = new CountDownLatch(1);
public SiteToSiteRestApiClient(final SSLContext sslContext, final HttpProxy proxy, final EventReporter eventReporter) {
this.sslContext = sslContext;
this.proxy = proxy;
this.eventReporter = eventReporter;
ttlExtendTaskExecutor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread(final Runnable r) {
final Thread thread = defaultFactory.newThread(r);
thread.setName(Thread.currentThread().getName() + " TTLExtend");
thread.setDaemon(true);
return thread;
}
});
}
@Override
public void close() throws IOException {
stopExtendingTtl();
closeSilently(httpClient);
closeSilently(httpAsyncClient);
}
private CloseableHttpClient getHttpClient() {
if (httpClient == null) {
setupClient();
}
return httpClient;
}
private CloseableHttpAsyncClient getHttpAsyncClient() {
if (httpAsyncClient == null) {
setupAsyncClient();
}
return httpAsyncClient;
}
private RequestConfig getRequestConfig() {
if (requestConfig == null) {
setupRequestConfig();
}
return requestConfig;
}
private CredentialsProvider getCredentialsProvider() {
if (credentialsProvider == null) {
setupCredentialsProvider();
}
return credentialsProvider;
}
private void setupRequestConfig() {
final RequestConfig.Builder requestConfigBuilder = RequestConfig.custom()
.setConnectionRequestTimeout(connectTimeoutMillis)
.setConnectTimeout(connectTimeoutMillis)
.setSocketTimeout(readTimeoutMillis);
if (localAddress != null) {
requestConfigBuilder.setLocalAddress(localAddress);
}
if (proxy != null) {
requestConfigBuilder.setProxy(proxy.getHttpHost());
}
requestConfig = requestConfigBuilder.build();
}
private void setupCredentialsProvider() {
credentialsProvider = new BasicCredentialsProvider();
if (proxy != null) {
if (!isEmpty(proxy.getUsername()) && !isEmpty(proxy.getPassword())) {
credentialsProvider.setCredentials(
new AuthScope(proxy.getHttpHost()),
new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword()));
}
}
}
private void setupClient() {
final HttpClientBuilder clientBuilder = HttpClients.custom();
if (sslContext != null) {
clientBuilder.setSslcontext(sslContext);
clientBuilder.addInterceptorFirst(new HttpsResponseInterceptor());
}
httpClient = clientBuilder
.setDefaultCredentialsProvider(getCredentialsProvider()).build();
}
private void setupAsyncClient() {
final HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
if (sslContext != null) {
clientBuilder.setSSLContext(sslContext);
clientBuilder.addInterceptorFirst(new HttpsResponseInterceptor());
}
httpAsyncClient = clientBuilder.setDefaultCredentialsProvider(getCredentialsProvider()).build();
httpAsyncClient.start();
}
private class HttpsResponseInterceptor implements HttpResponseInterceptor {
@Override
public void process(final HttpResponse response, final HttpContext httpContext) throws HttpException, IOException {
final HttpCoreContext coreContext = HttpCoreContext.adapt(httpContext);
final HttpInetConnection conn = coreContext.getConnection(HttpInetConnection.class);
if (!conn.isOpen()) {
return;
}
final SSLSession sslSession;
if (conn instanceof ManagedHttpClientConnection) {
sslSession = ((ManagedHttpClientConnection) conn).getSSLSession();
} else if (conn instanceof ManagedNHttpClientConnection) {
sslSession = ((ManagedNHttpClientConnection) conn).getSSLSession();
} else {
throw new RuntimeException("Unexpected connection type was used, " + conn);
}
if (sslSession != null) {
final Certificate[] certChain = sslSession.getPeerCertificates();
if (certChain == null || certChain.length == 0) {
throw new SSLPeerUnverifiedException("No certificates found");
}
try {
final X509Certificate cert = CertificateUtils.convertAbstractX509Certificate(certChain[0]);
trustedPeerDn = cert.getSubjectDN().getName().trim();
} catch (final CertificateException e) {
final String msg = "Could not extract subject DN from SSL session peer certificate";
logger.warn(msg);
eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, msg);
throw new SSLPeerUnverifiedException(msg);
}
}
}
}
/**
* Parse the clusterUrls String, and try each URL in clusterUrls one by one to get a controller resource
* from those remote NiFi instances until a controller is successfully returned or try out all URLs.
* After this method execution, the base URL is set with the successful URL.
* @param clusterUrls url of the remote NiFi instance, multiple urls can be specified in comma-separated format
* @throws IllegalArgumentException when it fails to parse the URLs string,
* URLs string contains multiple protocols (http and https mix),
* or none of URL is specified.
*/
public ControllerDTO getController(final String clusterUrls) throws IOException {
return getController(parseClusterUrls(clusterUrls));
}
/**
* Try each URL in clusterUrls one by one to get a controller resource
* from those remote NiFi instances until a controller is successfully returned or try out all URLs.
* After this method execution, the base URL is set with the successful URL.
*/
public ControllerDTO getController(final Set<String> clusterUrls) throws IOException {
IOException lastException = null;
for (final String clusterUrl : clusterUrls) {
// The url may not be normalized if it passed directly without parsed with parseClusterUrls.
setBaseUrl(resolveBaseUrl(clusterUrl));
try {
return getController();
} catch (IOException e) {
lastException = e;
logger.warn("Failed to get controller from " + clusterUrl + " due to " + e);
if (logger.isDebugEnabled()) {
logger.debug("", e);
}
}
}
if (clusterUrls.size() > 1) {
throw new IOException("Tried all cluster URLs but none of those was accessible. Last Exception was " + lastException, lastException);
}
throw lastException;
}
private ControllerDTO getController() throws IOException {
try {
final HttpGet get = createGetControllerRequest();
return execute(get, ControllerEntity.class).getController();
} catch (final HttpGetFailedException e) {
if (RESPONSE_CODE_NOT_FOUND == e.getResponseCode()) {
logger.debug("getController received NOT_FOUND, trying to access the old NiFi version resource url...");
final HttpGet get = createGet("/controller");
return execute(get, ControllerEntity.class).getController();
}
throw e;
}
}
private HttpGet createGetControllerRequest() {
final HttpGet get = createGet("/site-to-site");
get.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
return get;
}
public Collection<PeerDTO> getPeers() throws IOException {
final HttpGet get = createGet("/site-to-site/peers");
get.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
return execute(get, PeersEntity.class).getPeers();
}
public String initiateTransaction(final TransferDirection direction, final String portId) throws IOException {
final String portType = TransferDirection.RECEIVE.equals(direction) ? "output-ports" : "input-ports";
logger.debug("initiateTransaction handshaking portType={}, portId={}", portType, portId);
final HttpPost post = createPost("/data-transfer/" + portType + "/" + portId + "/transactions");
post.setHeader("Accept", "application/json");
post.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
setHandshakeProperties(post);
final HttpResponse response;
if (TransferDirection.RECEIVE.equals(direction)) {
response = initiateTransactionForReceive(post);
} else {
response = initiateTransactionForSend(post);
}
final int responseCode = response.getStatusLine().getStatusCode();
logger.debug("initiateTransaction responseCode={}", responseCode);
String transactionUrl;
switch (responseCode) {
case RESPONSE_CODE_CREATED:
EntityUtils.consume(response.getEntity());
transactionUrl = readTransactionUrl(response);
if (isEmpty(transactionUrl)) {
throw new ProtocolException("Server returned RESPONSE_CODE_CREATED without Location header");
}
final Header transportProtocolVersionHeader = response.getFirstHeader(HttpHeaders.PROTOCOL_VERSION);
if (transportProtocolVersionHeader == null) {
throw new ProtocolException("Server didn't return confirmed protocol version");
}
final Integer protocolVersionConfirmedByServer = Integer.valueOf(transportProtocolVersionHeader.getValue());
logger.debug("Finished version negotiation, protocolVersionConfirmedByServer={}", protocolVersionConfirmedByServer);
transportProtocolVersionNegotiator.setVersion(protocolVersionConfirmedByServer);
final Header serverTransactionTtlHeader = response.getFirstHeader(HttpHeaders.SERVER_SIDE_TRANSACTION_TTL);
if (serverTransactionTtlHeader == null) {
throw new ProtocolException("Server didn't return " + HttpHeaders.SERVER_SIDE_TRANSACTION_TTL);
}
serverTransactionTtl = Integer.parseInt(serverTransactionTtlHeader.getValue());
break;
default:
try (InputStream content = response.getEntity().getContent()) {
throw handleErrResponse(responseCode, content);
}
}
logger.debug("initiateTransaction handshaking finished, transactionUrl={}", transactionUrl);
return transactionUrl;
}
/**
* Initiate a transaction for receiving data.
* @param post a POST request to establish transaction
* @return POST request response
* @throws IOException thrown if the post request failed
*/
private HttpResponse initiateTransactionForReceive(final HttpPost post) throws IOException {
return getHttpClient().execute(post);
}
/**
* <p>
* Initiate a transaction for sending data.
* </p>
*
* <p>
* If a proxy server requires auth, the proxy server returns 407 response with available auth schema such as basic or digest.
* Then client has to resend the same request with its credential added.
* This mechanism is problematic for sending data from NiFi.
* </p>
*
* <p>
* In order to resend a POST request with auth param,
* NiFi has to either read flow-file contents to send again, or keep the POST body somewhere.
* If we store that in memory, it would causes OOM, or storing it on disk slows down performance.
* Rolling back processing session would be overkill.
* Reading flow-file contents only when it's ready to send in a streaming way is ideal.
* </p>
*
* <p>
* Additionally, the way proxy authentication is done is vary among Proxy server software.
* Some requires 407 and resend cycle for every requests, while others keep a connection between a client and
* the proxy server, then consecutive requests skip auth steps.
* The problem is, that how should we behave is only told after sending a request to the proxy.
* </p>
*
* In order to handle above concerns correctly and efficiently, this method do the followings:
*
* <ol>
* <li>Send a GET request to controller resource, to initiate an HttpAsyncClient. The instance will be used for further requests.
* This is not required by the Site-to-Site protocol, but it can setup proxy auth state safely.</li>
* <li>Send a POST request to initiate a transaction. While doing so, it captures how a proxy server works.
* If 407 and resend cycle occurs here, it implies that we need to do the same thing again when we actually send the data.
* Because if the proxy keeps using the same connection and doesn't require an auth step, it doesn't do so here.</li>
* <li>Then this method stores whether the final POST request should wait for the auth step.
* So that {@link #openConnectionForSend} can determine when to produce contents.</li>
* </ol>
*
* <p>
* The above special sequence is only executed when a proxy instance is set, and its username is set.
* </p>
*
* @param post a POST request to establish transaction
* @return POST request response
* @throws IOException thrown if the post request failed
*/
private HttpResponse initiateTransactionForSend(final HttpPost post) throws IOException {
if (shouldCheckProxyAuth()) {
final CloseableHttpAsyncClient asyncClient = getHttpAsyncClient();
final HttpGet get = createGetControllerRequest();
final Future<HttpResponse> getResult = asyncClient.execute(get, null);
try {
final HttpResponse getResponse = getResult.get(readTimeoutMillis, TimeUnit.MILLISECONDS);
logger.debug("Proxy auth check has done. getResponse={}", getResponse.getStatusLine());
} catch (final ExecutionException e) {
logger.debug("Something has happened at get controller requesting thread for proxy auth check. {}", e.getMessage());
throw toIOException(e);
} catch (TimeoutException | InterruptedException e) {
throw new IOException(e);
}
}
final HttpAsyncRequestProducer asyncRequestProducer = new HttpAsyncRequestProducer() {
private boolean requestHasBeenReset = false;
@Override
public HttpHost getTarget() {
return URIUtils.extractHost(post.getURI());
}
@Override
public HttpRequest generateRequest() throws IOException, HttpException {
final BasicHttpEntity entity = new BasicHttpEntity();
post.setEntity(entity);
return post;
}
@Override
public void produceContent(ContentEncoder encoder, IOControl ioctrl) throws IOException {
encoder.complete();
if (shouldCheckProxyAuth() && requestHasBeenReset) {
logger.debug("Produced content again, assuming the proxy server requires authentication.");
proxyAuthRequiresResend.set(true);
}
}
@Override
public void requestCompleted(HttpContext context) {
debugProxyAuthState(context);
}
@Override
public void failed(Exception ex) {
final String msg = String.format("Failed to create transaction for %s", post.getURI());
logger.error(msg, ex);
eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, msg);
}
@Override
public boolean isRepeatable() {
return true;
}
@Override
public void resetRequest() throws IOException {
requestHasBeenReset = true;
}
@Override
public void close() throws IOException {
}
};
final Future<HttpResponse> responseFuture = getHttpAsyncClient().execute(asyncRequestProducer, new BasicAsyncResponseConsumer(), null);
final HttpResponse response;
try {
response = responseFuture.get(readTimeoutMillis, TimeUnit.MILLISECONDS);
} catch (final ExecutionException e) {
logger.debug("Something has happened at initiate transaction requesting thread. {}", e.getMessage());
throw toIOException(e);
} catch (TimeoutException | InterruptedException e) {
throw new IOException(e);
}
return response;
}
/**
* Print AuthState in HttpContext for debugging purpose.
* <p>
* If the proxy server requires 407 and resend cycle, this method logs as followings, for Basic Auth:
* <ul><li>state:UNCHALLENGED;</li>
* <li>state:CHALLENGED;auth scheme:basic;credentials present</li></ul>
* </p>
* <p>
* For Digest Auth:
* <ul><li>state:UNCHALLENGED;</li>
* <li>state:CHALLENGED;auth scheme:digest;credentials present</li></ul>
* </p>
* <p>
* But if the proxy uses the same connection, it doesn't return 407, in such case
* this method is called only once with:
* <ul><li>state:UNCHALLENGED</li></ul>
* </p>
*/
private void debugProxyAuthState(HttpContext context) {
final AuthState proxyAuthState;
if (shouldCheckProxyAuth()
&& logger.isDebugEnabled()
&& (proxyAuthState = (AuthState)context.getAttribute("http.auth.proxy-scope")) != null){
logger.debug("authProxyScope={}", proxyAuthState);
}
}
private IOException toIOException(ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
return (IOException) cause;
} else {
return new IOException(cause);
}
}
private boolean shouldCheckProxyAuth() {
return proxy != null && !isEmpty(proxy.getUsername());
}
public boolean openConnectionForReceive(final String transactionUrl, final Peer peer) throws IOException {
final HttpGet get = createGet(transactionUrl + "/flow-files");
// Set uri so that it'll be used as transit uri.
((HttpCommunicationsSession)peer.getCommunicationsSession()).setDataTransferUrl(get.getURI().toString());
get.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
setHandshakeProperties(get);
final CloseableHttpResponse response = getHttpClient().execute(get);
final int responseCode = response.getStatusLine().getStatusCode();
logger.debug("responseCode={}", responseCode);
boolean keepItOpen = false;
try {
switch (responseCode) {
case RESPONSE_CODE_OK:
logger.debug("Server returned RESPONSE_CODE_OK, indicating there was no data.");
EntityUtils.consume(response.getEntity());
return false;
case RESPONSE_CODE_ACCEPTED:
final InputStream httpIn = response.getEntity().getContent();
final InputStream streamCapture = new InputStream() {
boolean closed = false;
@Override
public int read() throws IOException {
if (closed) {
return -1;
}
final int r = httpIn.read();
if (r < 0) {
closed = true;
logger.debug("Reached to end of input stream. Closing resources...");
stopExtendingTtl();
closeSilently(httpIn);
closeSilently(response);
}
return r;
}
};
((HttpInput) peer.getCommunicationsSession().getInput()).setInputStream(streamCapture);
startExtendingTtl(transactionUrl, httpIn, response);
keepItOpen = true;
return true;
default:
try (InputStream content = response.getEntity().getContent()) {
throw handleErrResponse(responseCode, content);
}
}
} finally {
if (!keepItOpen) {
response.close();
}
}
}
public void openConnectionForSend(final String transactionUrl, final Peer peer) throws IOException {
final CommunicationsSession commSession = peer.getCommunicationsSession();
final String flowFilesPath = transactionUrl + "/flow-files";
final HttpPost post = createPost(flowFilesPath);
// Set uri so that it'll be used as transit uri.
((HttpCommunicationsSession)peer.getCommunicationsSession()).setDataTransferUrl(post.getURI().toString());
post.setHeader("Content-Type", "application/octet-stream");
post.setHeader("Accept", "text/plain");
post.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
setHandshakeProperties(post);
final CountDownLatch initConnectionLatch = new CountDownLatch(1);
final URI requestUri = post.getURI();
final PipedOutputStream outputStream = new PipedOutputStream();
final PipedInputStream inputStream = new PipedInputStream(outputStream, DATA_PACKET_CHANNEL_READ_BUFFER_SIZE);
final ReadableByteChannel dataPacketChannel = Channels.newChannel(inputStream);
final HttpAsyncRequestProducer asyncRequestProducer = new HttpAsyncRequestProducer() {
private final ByteBuffer buffer = ByteBuffer.allocate(DATA_PACKET_CHANNEL_READ_BUFFER_SIZE);
private int totalRead = 0;
private int totalProduced = 0;
private boolean requestHasBeenReset = false;
@Override
public HttpHost getTarget() {
return URIUtils.extractHost(requestUri);
}
@Override
public HttpRequest generateRequest() throws IOException, HttpException {
// Pass the output stream so that Site-to-Site client thread can send
// data packet through this connection.
logger.debug("sending data to {} has started...", flowFilesPath);
((HttpOutput) commSession.getOutput()).setOutputStream(outputStream);
initConnectionLatch.countDown();
final BasicHttpEntity entity = new BasicHttpEntity();
entity.setChunked(true);
entity.setContentType("application/octet-stream");
post.setEntity(entity);
return post;
}
private final AtomicBoolean bufferHasRemainingData = new AtomicBoolean(false);
/**
* If the proxy server requires authentication, the same POST request has to be sent again.
* The first request will result 407, then the next one will be sent with auth headers and actual data.
* This method produces a content only when it's need to be sent, to avoid producing the flow-file contents twice.
* Whether we need to wait auth is determined heuristically by the previous POST request which creates transaction.
* See {@link SiteToSiteRestApiClient#initiateTransactionForSend(HttpPost)} for further detail.
*/
@Override
public void produceContent(final ContentEncoder encoder, final IOControl ioControl) throws IOException {
if (shouldCheckProxyAuth() && proxyAuthRequiresResend.get() && !requestHasBeenReset) {
logger.debug("Need authentication with proxy server. Postpone producing content.");
encoder.complete();
return;
}
if (bufferHasRemainingData.get()) {
// If there's remaining buffer last time, send it first.
writeBuffer(encoder);
if (bufferHasRemainingData.get()) {
return;
}
}
int read;
// This read() blocks until data becomes available,
// or corresponding outputStream is closed.
if ((read = dataPacketChannel.read(buffer)) > -1) {
logger.trace("Read {} bytes from dataPacketChannel. {}", read, flowFilesPath);
totalRead += read;
buffer.flip();
writeBuffer(encoder);
} else {
final long totalWritten = commSession.getOutput().getBytesWritten();
logger.debug("sending data to {} has reached to its end. produced {} bytes by reading {} bytes from channel. {} bytes written in this transaction.",
flowFilesPath, totalProduced, totalRead, totalWritten);
if (totalRead != totalWritten || totalProduced != totalWritten) {
final String msg = "Sending data to %s has reached to its end, but produced : read : wrote byte sizes (%d : %d : %d) were not equal. Something went wrong.";
throw new RuntimeException(String.format(msg, flowFilesPath, totalProduced, totalRead, totalWritten));
}
transferDataLatch.countDown();
encoder.complete();
dataPacketChannel.close();
}
}
private void writeBuffer(ContentEncoder encoder) throws IOException {
while (buffer.hasRemaining()) {
final int written = encoder.write(buffer);
logger.trace("written {} bytes to encoder.", written);
if (written == 0) {
logger.trace("Buffer still has remaining. {}", buffer);
bufferHasRemainingData.set(true);
return;
}
totalProduced += written;
}
bufferHasRemainingData.set(false);
buffer.clear();
}
@Override
public void requestCompleted(final HttpContext context) {
logger.debug("Sending data to {} completed.", flowFilesPath);
debugProxyAuthState(context);
}
@Override
public void failed(final Exception ex) {
final String msg = String.format("Failed to send data to %s due to %s", flowFilesPath, ex.toString());
logger.error(msg, ex);
eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, msg);
}
@Override
public boolean isRepeatable() {
// In order to pass authentication, request has to be repeatable.
return true;
}
@Override
public void resetRequest() throws IOException {
logger.debug("Sending data request to {} has been reset...", flowFilesPath);
requestHasBeenReset = true;
}
@Override
public void close() throws IOException {
logger.debug("Closing sending data request to {}", flowFilesPath);
closeSilently(outputStream);
closeSilently(dataPacketChannel);
stopExtendingTtl();
}
};
postResult = getHttpAsyncClient().execute(asyncRequestProducer, new BasicAsyncResponseConsumer(), null);
try {
// Need to wait the post request actually started so that we can write to its output stream.
if (!initConnectionLatch.await(connectTimeoutMillis, TimeUnit.MILLISECONDS)) {
throw new IOException("Awaiting initConnectionLatch has been timeout.");
}
// Started.
transferDataLatch = new CountDownLatch(1);
startExtendingTtl(transactionUrl, dataPacketChannel, null);
} catch (final InterruptedException e) {
throw new IOException("Awaiting initConnectionLatch has been interrupted.", e);
}
}
public void finishTransferFlowFiles(final CommunicationsSession commSession) throws IOException {
if (postResult == null) {
new IllegalStateException("Data transfer has not started yet.");
}
// No more data can be sent.
// Close PipedOutputStream so that dataPacketChannel doesn't blocked.
// If we don't close this output stream, then PipedInputStream loops infinitely at read().
commSession.getOutput().getOutputStream().close();
logger.debug("{} FinishTransferFlowFiles no more data can be sent", this);
try {
if (!transferDataLatch.await(requestExpirationMillis, TimeUnit.MILLISECONDS)) {
throw new IOException("Awaiting transferDataLatch has been timeout.");
}
} catch (final InterruptedException e) {
throw new IOException("Awaiting transferDataLatch has been interrupted.", e);
}
stopExtendingTtl();
final HttpResponse response;
try {
response = postResult.get(readTimeoutMillis, TimeUnit.MILLISECONDS);
} catch (final ExecutionException e) {
logger.debug("Something has happened at sending data thread. {}", e.getMessage());
throw toIOException(e);
} catch (TimeoutException | InterruptedException e) {
throw new IOException(e);
}
final int responseCode = response.getStatusLine().getStatusCode();
switch (responseCode) {
case RESPONSE_CODE_ACCEPTED:
final String receivedChecksum = EntityUtils.toString(response.getEntity());
((HttpInput) commSession.getInput()).setInputStream(new ByteArrayInputStream(receivedChecksum.getBytes()));
((HttpCommunicationsSession) commSession).setChecksum(receivedChecksum);
logger.debug("receivedChecksum={}", receivedChecksum);
break;
default:
try (InputStream content = response.getEntity().getContent()) {
throw handleErrResponse(responseCode, content);
}
}
}
private void startExtendingTtl(final String transactionUrl, final Closeable stream, final CloseableHttpResponse response) {
if (ttlExtendingFuture != null) {
// Already started.
return;
}
logger.debug("Starting extending TTL thread...");
extendingApiClient = new SiteToSiteRestApiClient(sslContext, proxy, EventReporter.NO_OP);
extendingApiClient.transportProtocolVersionNegotiator = this.transportProtocolVersionNegotiator;
extendingApiClient.connectTimeoutMillis = this.connectTimeoutMillis;
extendingApiClient.readTimeoutMillis = this.readTimeoutMillis;
extendingApiClient.localAddress = this.localAddress;
final int extendFrequency = serverTransactionTtl / 2;
ttlExtendingFuture = ttlExtendTaskExecutor.scheduleWithFixedDelay(() -> {
try {
extendingApiClient.extendTransaction(transactionUrl);
} catch (final Exception e) {
logger.warn("Failed to extend transaction ttl", e);
try {
// Without disconnecting, Site-to-Site client keep reading data packet,
// while server has already rollback.
this.close();
} catch (final IOException ec) {
logger.warn("Failed to close", e);
}
}
}, extendFrequency, extendFrequency, TimeUnit.SECONDS);
}
private void closeSilently(final Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (final IOException e) {
logger.warn("Got an exception when closing {}: {}", closeable, e.getMessage());
if (logger.isDebugEnabled()) {
logger.warn("", e);
}
}
}
public TransactionResultEntity extendTransaction(final String transactionUrl) throws IOException {
logger.debug("Sending extendTransaction request to transactionUrl: {}", transactionUrl);
final HttpPut put = createPut(transactionUrl);
put.setHeader("Accept", "application/json");
put.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
setHandshakeProperties(put);
try (final CloseableHttpResponse response = getHttpClient().execute(put)) {
final int responseCode = response.getStatusLine().getStatusCode();
logger.debug("extendTransaction responseCode={}", responseCode);
try (final InputStream content = response.getEntity().getContent()) {
switch (responseCode) {
case RESPONSE_CODE_OK:
return readResponse(content);
default:
throw handleErrResponse(responseCode, content);
}
}
}
}
private void stopExtendingTtl() {
if (!ttlExtendTaskExecutor.isShutdown()) {
ttlExtendTaskExecutor.shutdown();
}
if (ttlExtendingFuture != null && !ttlExtendingFuture.isCancelled()) {
logger.debug("Cancelling extending ttl...");
ttlExtendingFuture.cancel(true);
}
closeSilently(extendingApiClient);
}
private IOException handleErrResponse(final int responseCode, final InputStream in) throws IOException {
if (in == null) {
return new IOException("Unexpected response code: " + responseCode);
}
final TransactionResultEntity errEntity = readResponse(in);
final ResponseCode errCode = ResponseCode.fromCode(errEntity.getResponseCode());
switch (errCode) {
case UNKNOWN_PORT:
return new UnknownPortException(errEntity.getMessage());
case PORT_NOT_IN_VALID_STATE:
return new PortNotRunningException(errEntity.getMessage());
default:
switch (responseCode) {
case RESPONSE_CODE_FORBIDDEN :
return new HandshakeException(errEntity.getMessage());
default:
return new IOException("Unexpected response code: " + responseCode + " errCode:" + errCode + " errMessage:" + errEntity.getMessage());
}
}
}
private TransactionResultEntity readResponse(final InputStream inputStream) throws IOException {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
StreamUtils.copy(inputStream, bos);
String responseMessage = null;
try {
responseMessage = new String(bos.toByteArray(), "UTF-8");
logger.debug("readResponse responseMessage={}", responseMessage);
final ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(responseMessage, TransactionResultEntity.class);
} catch (JsonParseException | JsonMappingException e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to parse JSON.", e);
}
final TransactionResultEntity entity = new TransactionResultEntity();
entity.setResponseCode(ResponseCode.ABORT.getCode());
entity.setMessage(responseMessage);
return entity;
}
}
private String readTransactionUrl(final HttpResponse response) {
final Header locationUriIntentHeader = response.getFirstHeader(LOCATION_URI_INTENT_NAME);
logger.debug("locationUriIntentHeader={}", locationUriIntentHeader);
if (locationUriIntentHeader != null && LOCATION_URI_INTENT_VALUE.equals(locationUriIntentHeader.getValue())) {
final Header transactionUrl = response.getFirstHeader(LOCATION_HEADER_NAME);
logger.debug("transactionUrl={}", transactionUrl);
if (transactionUrl != null) {
return transactionUrl.getValue();
}
}
return null;
}
private void setHandshakeProperties(final HttpRequestBase httpRequest) {
if (compress) {
httpRequest.setHeader(HANDSHAKE_PROPERTY_USE_COMPRESSION, "true");
}
if (requestExpirationMillis > 0) {
httpRequest.setHeader(HANDSHAKE_PROPERTY_REQUEST_EXPIRATION, String.valueOf(requestExpirationMillis));
}
if (batchCount > 0) {
httpRequest.setHeader(HANDSHAKE_PROPERTY_BATCH_COUNT, String.valueOf(batchCount));
}
if (batchSize > 0) {
httpRequest.setHeader(HANDSHAKE_PROPERTY_BATCH_SIZE, String.valueOf(batchSize));
}
if (batchDurationMillis > 0) {
httpRequest.setHeader(HANDSHAKE_PROPERTY_BATCH_DURATION, String.valueOf(batchDurationMillis));
}
}
private URI getUri(final String path) {
final URI url;
try {
if (HTTP_ABS_URL.matcher(path).find()) {
url = new URI(path);
} else {
if (StringUtils.isEmpty(getBaseUrl())) {
throw new IllegalStateException("API baseUrl is not resolved yet, call setBaseUrl or resolveBaseUrl before sending requests with relative path.");
}
url = new URI(baseUrl + path);
}
} catch (final URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage());
}
return url;
}
private HttpGet createGet(final String path) {
final URI url = getUri(path);
final HttpGet get = new HttpGet(url);
get.setConfig(getRequestConfig());
return get;
}
private HttpPost createPost(final String path) {
final URI url = getUri(path);
final HttpPost post = new HttpPost(url);
post.setConfig(getRequestConfig());
return post;
}
private HttpPut createPut(final String path) {
final URI url = getUri(path);
final HttpPut put = new HttpPut(url);
put.setConfig(getRequestConfig());
return put;
}
private HttpDelete createDelete(final String path) {
final URI url = getUri(path);
final HttpDelete delete = new HttpDelete(url);
delete.setConfig(getRequestConfig());
return delete;
}
private String execute(final HttpGet get) throws IOException {
final CloseableHttpClient httpClient = getHttpClient();
if (logger.isTraceEnabled()) {
Arrays.stream(get.getAllHeaders()).forEach(h -> logger.debug("REQ| {}", h));
}
try (final CloseableHttpResponse response = httpClient.execute(get)) {
if (logger.isTraceEnabled()) {
Arrays.stream(response.getAllHeaders()).forEach(h -> logger.debug("RES| {}", h));
}
final StatusLine statusLine = response.getStatusLine();
final int statusCode = statusLine.getStatusCode();
if (RESPONSE_CODE_OK != statusCode) {
throw new HttpGetFailedException(statusCode, statusLine.getReasonPhrase(), null);
}
final HttpEntity entity = response.getEntity();
final String responseMessage = EntityUtils.toString(entity);
return responseMessage;
}
}
public class HttpGetFailedException extends IOException {
private static final long serialVersionUID = 7920714957269466946L;
private final int responseCode;
private final String responseMessage;
private final String explanation;
public HttpGetFailedException(final int responseCode, final String responseMessage, final String explanation) {
super("response code " + responseCode + ":" + responseMessage + " with explanation: " + explanation);
this.responseCode = responseCode;
this.responseMessage = responseMessage;
this.explanation = explanation;
}
public int getResponseCode() {
return responseCode;
}
public String getDescription() {
return !isEmpty(explanation) ? explanation : responseMessage;
}
}
private <T> T execute(final HttpGet get, final Class<T> entityClass) throws IOException {
get.setHeader("Accept", "application/json");
final String responseMessage = execute(get);
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
return mapper.readValue(responseMessage, entityClass);
} catch (JsonParseException e) {
final String msg = "Failed to parse Json. The specified URL " + baseUrl + " is not a proper remote NiFi endpoint for Site-to-Site communication.";
logger.warn("{} requestedUrl={}, response={}", msg, get.getURI(), responseMessage);
throw new IOException(msg, e);
}
}
public String getBaseUrl() {
return baseUrl;
}
/**
* Set the baseUrl as it is, without altering or adjusting the specified url string.
* If the url is specified by user input, and if it needs to be resolved with leniency,
* then use {@link #resolveBaseUrl(String)} method before passing it to this method.
* @param baseUrl url to set
*/
public void setBaseUrl(final String baseUrl) {
this.baseUrl = baseUrl;
}
public void setConnectTimeoutMillis(final int connectTimeoutMillis) {
this.connectTimeoutMillis = connectTimeoutMillis;
}
public void setReadTimeoutMillis(final int readTimeoutMillis) {
this.readTimeoutMillis = readTimeoutMillis;
}
public static String getFirstUrl(final String clusterUrlStr) {
if (clusterUrlStr == null) {
return null;
}
final int commaIndex = clusterUrlStr.indexOf(',');
if (commaIndex > -1) {
return clusterUrlStr.substring(0, commaIndex);
}
return clusterUrlStr;
}
/**
* Parse the comma-separated URLs string for the remote NiFi instances.
* @return A set containing one or more URLs
* @throws IllegalArgumentException when it fails to parse the URLs string,
* URLs string contains multiple protocols (http and https mix),
* or none of URL is specified.
*/
public static Set<String> parseClusterUrls(final String clusterUrlStr) {
final Set<String> urls = new LinkedHashSet<>();
if (clusterUrlStr != null && clusterUrlStr.length() > 0) {
Arrays.stream(clusterUrlStr.split(","))
.map(s -> s.trim())
.filter(s -> s.length() > 0)
.forEach(s -> {
validateUriString(s);
urls.add(resolveBaseUrl(s).intern());
});
}
if (urls.size() == 0) {
throw new IllegalArgumentException("Cluster URL was not specified.");
}
final Predicate<String> isHttps = url -> url.toLowerCase().startsWith("https:");
if (urls.stream().anyMatch(isHttps) && urls.stream().anyMatch(isHttps.negate())) {
throw new IllegalArgumentException("Different protocols are used in the cluster URLs " + clusterUrlStr);
}
return Collections.unmodifiableSet(urls);
}
private static void validateUriString(String s) {
// parse the uri
final URI uri;
try {
uri = URI.create(s);
} catch (final IllegalArgumentException e) {
throw new IllegalArgumentException("The specified remote process group URL is malformed: " + s);
}
// validate each part of the uri
if (uri.getScheme() == null || uri.getHost() == null) {
throw new IllegalArgumentException("The specified remote process group URL is malformed: " + s);
}
if (!(uri.getScheme().equalsIgnoreCase("http") || uri.getScheme().equalsIgnoreCase("https"))) {
throw new IllegalArgumentException("The specified remote process group URL is invalid because it is not http or https: " + s);
}
}
private static String resolveBaseUrl(final String clusterUrl) {
Objects.requireNonNull(clusterUrl, "clusterUrl cannot be null.");
final URI uri;
try {
uri = new URI(clusterUrl.trim());
} catch (final URISyntaxException e) {
throw new IllegalArgumentException("The specified URL is malformed: " + clusterUrl);
}
return resolveBaseUrl(uri);
}
/**
* Resolve NiFi API url with leniency. This method does following conversion on uri path:
* <ul>
* <li>/ to /nifi-api</li>
* <li>/nifi to /nifi-api</li>
* <li>/some/path/ to /some/path/nifi-api</li>
* </ul>
* @param clusterUrl url to be resolved
* @return resolved url
*/
private static String resolveBaseUrl(final URI clusterUrl) {
if (clusterUrl.getScheme() == null || clusterUrl.getHost() == null) {
throw new IllegalArgumentException("The specified URL is malformed: " + clusterUrl);
}
if (!(clusterUrl.getScheme().equalsIgnoreCase("http") || clusterUrl.getScheme().equalsIgnoreCase("https"))) {
throw new IllegalArgumentException("The specified URL is invalid because it is not http or https: " + clusterUrl);
}
String uriPath = clusterUrl.getPath().trim();
if (StringUtils.isEmpty(uriPath) || uriPath.equals("/")) {
uriPath = "/nifi";
} else if (uriPath.endsWith("/")) {
uriPath = uriPath.substring(0, uriPath.length() - 1);
}
if (uriPath.endsWith("/nifi")) {
uriPath += "-api";
} else if (!uriPath.endsWith("/nifi-api")) {
uriPath += "/nifi-api";
}
try {
return new URL(clusterUrl.getScheme(), clusterUrl.getHost(), clusterUrl.getPort(), uriPath).toURI().toString();
} catch (MalformedURLException|URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
public void setBaseUrl(final String scheme, final String host, final int port) {
setBaseUrl(scheme, host, port, "/nifi-api");
}
private void setBaseUrl(final String scheme, final String host, final int port, final String path) {
final String baseUri;
try {
baseUri = new URL(scheme, host, port, path).toURI().toString();
} catch (MalformedURLException|URISyntaxException e) {
throw new IllegalArgumentException(e);
}
this.setBaseUrl(baseUri);
}
public void setCompress(final boolean compress) {
this.compress = compress;
}
public void setLocalAddress(final InetAddress localAddress) {
this.localAddress = localAddress;
}
public void setRequestExpirationMillis(final long requestExpirationMillis) {
if (requestExpirationMillis < 0) {
throw new IllegalArgumentException("requestExpirationMillis can't be a negative value.");
}
this.requestExpirationMillis = requestExpirationMillis;
}
public void setBatchCount(final int batchCount) {
if (batchCount < 0) {
throw new IllegalArgumentException("batchCount can't be a negative value.");
}
this.batchCount = batchCount;
}
public void setBatchSize(final long batchSize) {
if (batchSize < 0) {
throw new IllegalArgumentException("batchSize can't be a negative value.");
}
this.batchSize = batchSize;
}
public void setBatchDurationMillis(final long batchDurationMillis) {
if (batchDurationMillis < 0) {
throw new IllegalArgumentException("batchDurationMillis can't be a negative value.");
}
this.batchDurationMillis = batchDurationMillis;
}
public Integer getTransactionProtocolVersion() {
return transportProtocolVersionNegotiator.getTransactionProtocolVersion();
}
public String getTrustedPeerDn() {
return this.trustedPeerDn;
}
public TransactionResultEntity commitReceivingFlowFiles(final String transactionUrl, final ResponseCode clientResponse, final String checksum) throws IOException {
logger.debug("Sending commitReceivingFlowFiles request to transactionUrl: {}, clientResponse={}, checksum={}",
transactionUrl, clientResponse, checksum);
stopExtendingTtl();
final StringBuilder urlBuilder = new StringBuilder(transactionUrl).append("?responseCode=").append(clientResponse.getCode());
if (ResponseCode.CONFIRM_TRANSACTION.equals(clientResponse)) {
urlBuilder.append("&checksum=").append(checksum);
}
final HttpDelete delete = createDelete(urlBuilder.toString());
delete.setHeader("Accept", "application/json");
delete.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
setHandshakeProperties(delete);
try (CloseableHttpResponse response = getHttpClient().execute(delete)) {
final int responseCode = response.getStatusLine().getStatusCode();
logger.debug("commitReceivingFlowFiles responseCode={}", responseCode);
try (InputStream content = response.getEntity().getContent()) {
switch (responseCode) {
case RESPONSE_CODE_OK:
return readResponse(content);
case RESPONSE_CODE_BAD_REQUEST:
return readResponse(content);
default:
throw handleErrResponse(responseCode, content);
}
}
}
}
public TransactionResultEntity commitTransferFlowFiles(final String transactionUrl, final ResponseCode clientResponse) throws IOException {
final String requestUrl = transactionUrl + "?responseCode=" + clientResponse.getCode();
logger.debug("Sending commitTransferFlowFiles request to transactionUrl: {}", requestUrl);
final HttpDelete delete = createDelete(requestUrl);
delete.setHeader("Accept", "application/json");
delete.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
setHandshakeProperties(delete);
try (CloseableHttpResponse response = getHttpClient().execute(delete)) {
final int responseCode = response.getStatusLine().getStatusCode();
logger.debug("commitTransferFlowFiles responseCode={}", responseCode);
try (InputStream content = response.getEntity().getContent()) {
switch (responseCode) {
case RESPONSE_CODE_OK:
return readResponse(content);
case RESPONSE_CODE_BAD_REQUEST:
return readResponse(content);
default:
throw handleErrResponse(responseCode, content);
}
}
}
}
}