From e8596ec86baf76746fe76a790d07cd2ac3206314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20Rucha=C5=82a?= Date: Wed, 6 Mar 2024 17:40:13 +0100 Subject: [PATCH] creating a batchSet using files instead of buffers kept in memory after exceeding the activation threshold --- .../api/client/batch/BatchChangeSetPart.java | 18 +- .../api/client/batch/BatchInputResource.java | 31 +++ .../core/batch/BatchChangeSetPartImpl.java | 42 ++-- .../olingo/odata2/core/batch/BatchHelper.java | 183 ++++++++++++++---- .../odata2/core/batch/BatchRequestWriter.java | 16 +- .../batch/DeleteOnCloseFileInputStream.java | 27 +++ 6 files changed, 242 insertions(+), 75 deletions(-) create mode 100644 odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchInputResource.java create mode 100644 odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/DeleteOnCloseFileInputStream.java diff --git a/odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchChangeSetPart.java b/odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchChangeSetPart.java index 93f86d5bc..5c7690803 100644 --- a/odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchChangeSetPart.java +++ b/odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchChangeSetPart.java @@ -18,10 +18,10 @@ ******************************************************************************/ package org.apache.olingo.odata2.api.client.batch; -import java.util.Map; - import org.apache.olingo.odata2.api.rt.RuntimeDelegate; +import java.util.Map; + /** * A BatchChangeSetPart *

BatchChangeSetPart represents a change request within a Change Set @@ -30,9 +30,7 @@ public abstract class BatchChangeSetPart { public abstract Map getHeaders(); - public abstract Object getBody(); - - public abstract byte[] getBodyAsBytes(); + public abstract BatchInputResource getBatchInputResource(); public abstract String getUri(); @@ -64,6 +62,14 @@ public static BatchChangeSetPartBuilder body(final byte[] body) { return newBuilder().body(body); } + /** + * @param body a change request body + * @return a new builder object + */ + public static BatchChangeSetPartBuilder body(final BatchInputResource body) { + return newBuilder().body(body); + } + /** * @param uri should not be null * @return a new builder object @@ -111,6 +117,8 @@ private static BatchChangeSetPartBuilder newInstance() { public abstract BatchChangeSetPartBuilder body(byte[] body); + public abstract BatchChangeSetPartBuilder body(BatchInputResource inputStream); + public abstract BatchChangeSetPartBuilder uri(String uri); public abstract BatchChangeSetPartBuilder method(String method); diff --git a/odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchInputResource.java b/odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchInputResource.java new file mode 100644 index 000000000..04bdab495 --- /dev/null +++ b/odata2-lib/odata-api/src/main/java/org/apache/olingo/odata2/api/client/batch/BatchInputResource.java @@ -0,0 +1,31 @@ +package org.apache.olingo.odata2.api.client.batch; + +import java.io.*; + +public class BatchInputResource implements Closeable { + + private final InputStream inputStream; + private final int size; + + public BatchInputResource(InputStream inputStream, int size) { + this.inputStream = inputStream; + this.size = size; + } + + public InputStream getInputStream() { + return inputStream; + } + + public int size() { + return size; + } + + @Override + public void close() throws IOException { + if (inputStream != null) { + inputStream.close(); + } + } + +} + diff --git a/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchChangeSetPartImpl.java b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchChangeSetPartImpl.java index be88a0bd1..c0ef0d104 100644 --- a/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchChangeSetPartImpl.java +++ b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchChangeSetPartImpl.java @@ -19,7 +19,9 @@ package org.apache.olingo.odata2.core.batch; import org.apache.olingo.odata2.api.client.batch.BatchChangeSetPart; +import org.apache.olingo.odata2.api.client.batch.BatchInputResource; +import java.io.ByteArrayInputStream; import java.nio.charset.Charset; import java.util.Collections; import java.util.HashMap; @@ -28,7 +30,7 @@ public class BatchChangeSetPartImpl extends BatchChangeSetPart { private String method; private Map headers = new HashMap(); - private Object body; + private BatchInputResource batchInputResource; private String uri; private String cntId; private static final String CHANGE_METHODS = "(PUT|POST|DELETE|MERGE|PATCH)"; @@ -39,21 +41,8 @@ public Map getHeaders() { } @Override - public String getBody() { - return body.toString(); - } - - @Override - public byte[] getBodyAsBytes() { - if(body == null) { - return new byte[0]; - } - Charset charset = getCharset(); - if (body instanceof byte[]) { - return (byte[]) body; //NOSONAR - } else { - return body.toString().getBytes(charset); - } + public BatchInputResource getBatchInputResource() { + return batchInputResource; } private Charset getCharset() { @@ -78,9 +67,10 @@ public String getContentId() { public class BatchChangeSetRequestBuilderImpl extends BatchChangeSetPartBuilder { private String method; private Map headers = new HashMap(); - private Object body; + private BatchInputResource batchInputResource; private String uri; private String contentId; + private String stringBody; @Override public BatchChangeSetPart build() { @@ -89,7 +79,8 @@ public BatchChangeSetPart build() { } BatchChangeSetPartImpl.this.method = method; BatchChangeSetPartImpl.this.headers = headers; - BatchChangeSetPartImpl.this.body = body; + BatchChangeSetPartImpl.this.batchInputResource = batchInputResource != null ? + batchInputResource : stringBodyToBatchInputResource(); BatchChangeSetPartImpl.this.uri = uri; BatchChangeSetPartImpl.this.cntId = contentId; return BatchChangeSetPartImpl.this; @@ -103,13 +94,24 @@ public BatchChangeSetPartBuilder headers(final Map headers) { @Override public BatchChangeSetPartBuilder body(final String body) { - this.body = body; + this.stringBody = body; return this; } + + private BatchInputResource stringBodyToBatchInputResource() { + byte[] bytes = stringBody.getBytes(getCharset()); + return new BatchInputResource(new ByteArrayInputStream(bytes), bytes.length); + } @Override public BatchChangeSetPartBuilder body(byte[] body) { - this.body = body; + this.batchInputResource = new BatchInputResource(new ByteArrayInputStream(body), body.length); + return this; + } + + @Override + public BatchChangeSetPartBuilder body(BatchInputResource batchInputStream) { + this.batchInputResource = batchInputStream; return this; } diff --git a/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchHelper.java b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchHelper.java index 3d2b78f1e..b67921d84 100644 --- a/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchHelper.java +++ b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchHelper.java @@ -19,16 +19,13 @@ package org.apache.olingo.odata2.core.batch; import org.apache.olingo.odata2.api.client.batch.BatchChangeSetPart; +import org.apache.olingo.odata2.api.client.batch.BatchInputResource; import org.apache.olingo.odata2.api.commons.HttpHeaders; import org.apache.olingo.odata2.api.processor.ODataResponse; import org.apache.olingo.odata2.core.commons.ContentType; import org.apache.olingo.odata2.core.exception.ODataRuntimeException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; @@ -119,29 +116,61 @@ private static void setDefaultValues(String contentType) { * Builder class to create the body and the header. */ static class BodyBuilder { + + private static final String OLINGO_TMPDIR_PROPERTY = "olingo.tmpdir"; + public static final int DEFAULT_SIZE = 8192; + private static final int THRESHOLD = DEFAULT_SIZE * 8; private final Charset CHARSET_ISO_8859_1 = Charset.forName("iso-8859-1"); + private ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_SIZE); - private boolean isClosed = false; + private File fileBuffer = null; - public byte[] getContent() { - isClosed = true; - byte[] tmp = new byte[buffer.position()]; - buffer.flip(); - buffer.get(tmp, 0, buffer.limit()); - return tmp; - } + private boolean isClosed = false; public InputStream getContentAsStream() { - return new ByteArrayInputStream(getContent()); + try { + return fileBuffer != null ? + new DeleteOnCloseFileInputStream(fileBuffer) : new ByteArrayInputStream(getBufferContent()); + } catch (IOException exception) { + throw new ODataRuntimeException(exception); + } } public String getContentAsString(Charset charset) { - return new String(getContent(), charset); + if (fileBuffer != null) { + InputStreamReader reader = null; + try { + reader = new InputStreamReader(new DeleteOnCloseFileInputStream(fileBuffer), charset); + + StringBuilder sb = new StringBuilder(); + char[] buffer = new char[DEFAULT_SIZE]; + int bytesCount; + while ((bytesCount = reader.read(buffer)) != -1) { + sb.append(buffer, 0, bytesCount); + } + reader.close(); + + return sb.toString(); + } catch (IOException e) { + throw new ODataRuntimeException(e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) {} + } + } + } + + return new String(getBufferContent(), charset); } public int getLength() { - return (buffer.limit() > buffer.position() ? buffer.limit(): buffer.position()); + if (fileBuffer == null) { + return (Math.max(buffer.limit(), buffer.position())); + } + return (int) fileBuffer.length(); } public BodyBuilder append(String string) { @@ -151,17 +180,97 @@ public BodyBuilder append(String string) { } private void put(byte[] b) { + put(new BatchInputResource(new ByteArrayInputStream(b), b.length)); + } + + private void put(BatchInputResource resource) { if(isClosed) { throw new RuntimeException("BodyBuilder is closed."); } - if(buffer.remaining() < b.length) { - buffer.flip(); - int newSize = (buffer.limit() * 2) + b.length; - ByteBuffer tmp = ByteBuffer.allocate(newSize); - tmp.put(buffer); - buffer = tmp; + + if (fileBuffer == null) { + if (buffer.remaining() < resource.size()) { + int newSize = (buffer.limit() * 2) + resource.size(); + if (newSize > THRESHOLD) { + fileBuffer = createTempFile(); + writeToFileBuffer(new ByteArrayInputStream(buffer.array(), 0, buffer.position()), + resource.getInputStream()); + buffer = null; + } else { + buffer.flip(); + ByteBuffer tmp = ByteBuffer.allocate(newSize); + tmp.put(buffer); + buffer = tmp; + writeToByteBuffer(resource.getInputStream()); + } + } else { + writeToByteBuffer(resource.getInputStream()); + } + } else { + writeToFileBuffer(resource.getInputStream()); } - buffer.put(b); + } + + File createTempFile() { + String tempDir = System.getProperty(OLINGO_TMPDIR_PROPERTY); + if (tempDir == null) { + tempDir = System.getProperty("java.io.tmpdir"); + } + + try { + return File.createTempFile("odata", "olingo", new File(tempDir)); + } catch (IOException e) { + throw new ODataRuntimeException(e); + } + } + + private void writeToByteBuffer(InputStream inputStream) { + int bytesCount; + try { + for (byte[] sbuf = new byte[DEFAULT_SIZE]; (bytesCount = inputStream.read(sbuf)) != -1;) { + buffer.put(sbuf, 0, bytesCount); + } + } catch (IOException e) { + throw new ODataRuntimeException(e); + } + } + + private void writeToFileBuffer(InputStream... inputStreams) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(fileBuffer, true); + for (InputStream inputStream : inputStreams) { + copyStream(inputStream, fos); + } + } catch (IOException e) { + throw new ODataRuntimeException(e); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) {} + } + } + } + + private void copyStream(InputStream inputStream, FileOutputStream outputStream) { + try { + int bytesRead; + for (byte[] sbuf = new byte[DEFAULT_SIZE]; (bytesRead = inputStream.read(sbuf)) != -1; ) { + outputStream.write(sbuf, 0, bytesRead); + } + outputStream.flush(); + } catch (IOException e) { + throw new ODataRuntimeException(e); + } + } + + private byte[] getBufferContent() { + isClosed = true; + byte[] tmp = new byte[buffer.position()]; + buffer.flip(); + buffer.get(tmp, 0, buffer.limit()); + return tmp; } public BodyBuilder append(int statusCode) { @@ -169,7 +278,7 @@ public BodyBuilder append(int statusCode) { } public BodyBuilder append(Body body) { - put(body.getContent()); + put(body.getBatchInputResource()); return this; } @@ -211,39 +320,33 @@ public int calculateLength(Object batchResponseBody) { static class Body { private static final int BUFFER_SIZE = 8192; public static final byte[] EMPTY_BYTES = new byte[0]; - private final byte[] content; + private final BatchInputResource batchInputResource; public Body(BatchChangeSetPart response) { - this.content = getBody(response); + this.batchInputResource = response.getBatchInputResource(); } public Body(ODataResponse response) { - this.content = getBody(response); + byte[] content = getBody(response); + this.batchInputResource = new BatchInputResource( + new ByteArrayInputStream(content), content.length); } public Body() { - this.content = EMPTY_BYTES; + this.batchInputResource = new BatchInputResource(new ByteArrayInputStream(EMPTY_BYTES), 0); setDefaultValues(ISO_ENCODING); } public int getLength() { - return content.length; + return batchInputResource.size(); } - public byte[] getContent() { - return content; + public BatchInputResource getBatchInputResource() { + return batchInputResource; } public boolean isEmpty() { - return content.length == 0; - } - - private byte[] getBody(final BatchChangeSetPart response) { - if (response == null || response.getBodyAsBytes() == null) { - return EMPTY_BYTES; - } - - return response.getBodyAsBytes(); + return batchInputResource.size() == 0; } private byte[] getBody(final ODataResponse response) { diff --git a/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestWriter.java b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestWriter.java index c050a1d20..e3def7e3a 100644 --- a/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestWriter.java +++ b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestWriter.java @@ -18,11 +18,6 @@ ******************************************************************************/ package org.apache.olingo.odata2.core.batch; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.List; -import java.util.Map; - import org.apache.olingo.odata2.api.client.batch.BatchChangeSet; import org.apache.olingo.odata2.api.client.batch.BatchChangeSetPart; import org.apache.olingo.odata2.api.client.batch.BatchPart; @@ -30,6 +25,10 @@ import org.apache.olingo.odata2.api.commons.HttpContentType; import org.apache.olingo.odata2.api.commons.HttpHeaders; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + public class BatchRequestWriter { private static final String REG_EX_BOUNDARY = "([a-zA-Z0-9_\\-\\.'\\+]{1,70})|\"([a-zA-Z0-9_\\-\\.'\\+\\s\\" + @@ -41,7 +40,7 @@ public class BatchRequestWriter { public static final String BOUNDARY_PREAMBLE = "changeset"; public static final String HTTP_1_1 = "HTTP/1.1"; private String batchBoundary; - private BatchHelper.BodyBuilder writer = new BatchHelper.BodyBuilder(); + private final BatchHelper.BodyBuilder writer = new BatchHelper.BodyBuilder(); public InputStream writeBatchRequest(final List batchParts, final String boundary) { if (boundary.matches(REG_EX_BOUNDARY)) { @@ -60,12 +59,9 @@ public InputStream writeBatchRequest(final List batchParts, final Str } writer.append("--").append(boundary).append("--"); - InputStream batchRequestBody; - batchRequestBody = new ByteArrayInputStream(writer.getContent()); - return batchRequestBody; + return writer.getContentAsStream(); } - private void appendChangeSet(final BatchChangeSet batchChangeSet) { String boundary = BatchHelper.generateBoundary(BOUNDARY_PREAMBLE); while (boundary.equals(batchBoundary) || !boundary.matches(REG_EX_BOUNDARY)) { diff --git a/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/DeleteOnCloseFileInputStream.java b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/DeleteOnCloseFileInputStream.java new file mode 100644 index 000000000..1bdcffc1b --- /dev/null +++ b/odata2-lib/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/DeleteOnCloseFileInputStream.java @@ -0,0 +1,27 @@ +package org.apache.olingo.odata2.core.batch; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class DeleteOnCloseFileInputStream extends FileInputStream { + + private File file; + + public DeleteOnCloseFileInputStream(File file) throws FileNotFoundException{ + super(file); + this.file = file; + } + + public void close() throws IOException { + try { + super.close(); + } finally { + if(file != null) { + file.delete(); + file = null; + } + } + } +}