/**
* Copyright 2016 StreamSets Inc.
*
* Licensed under 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 com.streamsets.pipeline.lib.sdcipc;
import com.streamsets.pipeline.api.Stage;
import com.streamsets.pipeline.api.impl.Utils;
import com.streamsets.pipeline.lib.http.HttpRequestFragmenter;
import org.apache.commons.io.output.ByteArrayOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class SdcIpcRequestFragmenter implements HttpRequestFragmenter {
@Override
public List<Stage.ConfigIssue> init(Stage.Context context) {
return new ArrayList<>();
}
@Override
public void destroy() {
}
//10100000
static final byte BASE_MAGIC_NUMBER = (byte) 0xa0;
//10100001
static final byte JSON1_MAGIC_NUMBER = BASE_MAGIC_NUMBER | (byte) 0x01;
static boolean copy(InputStream input, OutputStream output, int limit) throws IOException {
byte[] buffer = new byte[8024];
boolean eof = false;
while (limit > 0 && !eof) {
int readLimit = Math.min(buffer.length, limit);
int read = input.read(buffer, 0, readLimit);
eof = (read == -1);
if (!eof) {
output.write(buffer, 0, read);
limit -= read;
}
}
return eof;
}
static int findEndOfLastLineBeforeLimit(byte[] buffer, int limit) {
for (int i = limit - 1; i > 0; i--) {
// as we are going backwards, this will handle \r\n EOLs as well without producing extra EOLs
// and if a buffer ends in \r, the last line will be kept as incomplete until the next chunk.
if (buffer[i] == '\n') {
return i + 1; // including EOL character
}
}
return -1;
}
// max array size will be limit + 1 (because the magic byte is being added)
static byte[] extract(InputStream is, ByteArrayOutputStream overflowBuffer, int limit) throws IOException {
// the inputstream we get has been already stripped of the magic byte if first call
byte[] message;
if (copy(is, overflowBuffer, limit - overflowBuffer.size())) {
// got rest of payload without exceeding the max message size
if (overflowBuffer.size() == 0) {
// there is no more payload
message = null;
} else {
// extract the rest payload and prefix it with the magic byte
byte[] data = overflowBuffer.toByteArray();
message = new byte[data.length + 1];
message[0] = JSON1_MAGIC_NUMBER;
System.arraycopy(data, 0, message, 1, data.length);
overflowBuffer.reset();
}
} else {
// got partial payload, exceeded the max message size
byte[] data = overflowBuffer.toByteArray();
// find last full record in partial payload
int lastEOL = findEndOfLastLineBeforeLimit(data, limit);
if (lastEOL == -1) {
throw new IOException(Utils.format("Maximum message size '{}' exceeded", limit));
}
// extract payload up to last EOL and prefix with the magic byte
message = new byte[lastEOL + 1];
message[0] = JSON1_MAGIC_NUMBER;
System.arraycopy(data, 0, message, 1, lastEOL);
// put back in the stream buffer the portion of the payload that did not make it to the message
overflowBuffer.reset();
overflowBuffer.write(data, lastEOL, data.length - lastEOL);
}
return message;
}
// copy of com.streamsets.pipeline.stage.destination.sdcipc.Constants
static final String APPLICATION_BINARY = "application/binary";
static final String X_SDC_JSON1_FRAGMENTABLE_HEADER = "X-SDC-JSON1-FRAGMENTABLE";
@Override
public boolean validate(HttpServletRequest req, HttpServletResponse res) throws IOException {
boolean valid;
if (!APPLICATION_BINARY.equalsIgnoreCase(req.getContentType())) {
res.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "Unsupported content type: " + req.getContentType());
valid = false;
} else if (!"true".equalsIgnoreCase(req.getHeader(X_SDC_JSON1_FRAGMENTABLE_HEADER))) {
res.sendError(
HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
"Missing '" + X_SDC_JSON1_FRAGMENTABLE_HEADER + "' header"
);
valid = false;
} else {
valid = true;
}
return valid;
}
@Override
public List<byte[]> fragment(InputStream is, int fragmentSizeKB, int maxSizeKB) throws IOException {
int fragmentSizeB = fragmentSizeKB * 1000;
int maxSizeB = maxSizeKB * 1000;
return fragmentInternal(is, fragmentSizeB, maxSizeB);
}
List<byte[]> fragmentInternal(InputStream is, int fragmentSizeB, int maxSizeB) throws IOException {
List<byte[]> list = new ArrayList<>();
int size = 0;
int magicByte = is.read();
if (magicByte == -1) {
throw new IOException("Request has no data");
} else if ((((byte)magicByte) & JSON1_MAGIC_NUMBER) != JSON1_MAGIC_NUMBER) {
throw new IOException(Utils.format("Data is not JSON1, unsupported magic byte '{}'", magicByte));
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream(fragmentSizeB);
byte[] message = extract(is, baos, fragmentSizeB - 2); // to account for the magic byte and \r\n EOLs
while (message != null) {
size += message.length;
if (size > maxSizeB) {
throw new IOException(Utils.format("Maximum data size '{}' exceeded", maxSizeB));
}
list.add(message);
message = extract(is, baos, fragmentSizeB - 2);
}
}
return list;
}
}