Skip to content

Commit 60f14c1

Browse files
committed
✨ allow opening a LocalResponse from a stream
1 parent 5615c0a commit 60f14c1

15 files changed

Lines changed: 265 additions & 119 deletions

File tree

‎src/main/java/com/mindee/parsing/BaseLocalResponse.java‎

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package com.mindee.parsing;
22

33
import java.io.BufferedReader;
4+
import java.io.ByteArrayInputStream;
45
import java.io.File;
56
import java.io.IOException;
67
import java.io.InputStream;
78
import java.io.InputStreamReader;
9+
import java.io.Reader;
10+
import java.io.StringReader;
11+
import java.io.UncheckedIOException;
812
import java.nio.charset.StandardCharsets;
913
import java.nio.file.Files;
1014
import java.nio.file.Path;
1115
import java.security.InvalidKeyException;
16+
import java.security.MessageDigest;
1217
import java.security.NoSuchAlgorithmException;
13-
import java.util.stream.Collectors;
14-
import java.util.stream.Stream;
1518
import javax.crypto.Mac;
1619
import javax.crypto.spec.SecretKeySpec;
1720
import lombok.Getter;
@@ -25,25 +28,41 @@ public abstract class BaseLocalResponse {
2528
protected final byte[] file;
2629

2730
/**
28-
* Load from an {@link InputStream}.
31+
* Load from a string.
2932
*
3033
* @param input will be decoded as UTF-8.
3134
*/
32-
public BaseLocalResponse(InputStream input) {
33-
this.file = this
34-
.getBytes(new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)).lines());
35+
public BaseLocalResponse(String input) {
36+
if (input == null) {
37+
throw new IllegalArgumentException("Input string cannot be null.");
38+
}
39+
this.file = readToCleanUtf8Bytes(new StringReader(input));
40+
}
41+
42+
/**
43+
* Load from a byte array.
44+
*
45+
* @param input assumes UTF-8 encoding.
46+
*/
47+
public BaseLocalResponse(byte[] input) {
48+
if (input == null) {
49+
throw new IllegalArgumentException("Input byte array cannot be null.");
50+
}
51+
this.file = readToCleanUtf8Bytes(
52+
new InputStreamReader(new ByteArrayInputStream(input), StandardCharsets.UTF_8)
53+
);
3554
}
3655

3756
/**
38-
* Load from a {@link String}.
57+
* Load from an {@link InputStream}.
3958
*
4059
* @param input will be decoded as UTF-8.
4160
*/
42-
public BaseLocalResponse(String input) {
43-
if (input == null || input.isEmpty()) {
44-
throw new IllegalArgumentException("Input string cannot be empty or null.");
61+
public BaseLocalResponse(InputStream input) {
62+
if (input == null) {
63+
throw new IllegalArgumentException("Input stream cannot be null.");
4564
}
46-
this.file = input.getBytes(StandardCharsets.UTF_8);
65+
this.file = readToCleanUtf8Bytes(new InputStreamReader(input, StandardCharsets.UTF_8));
4766
}
4867

4968
/**
@@ -52,7 +71,12 @@ public BaseLocalResponse(String input) {
5271
* @param input will be decoded as UTF-8.
5372
*/
5473
public BaseLocalResponse(File input) throws IOException {
55-
this.file = this.getBytes(Files.lines(input.toPath(), StandardCharsets.UTF_8));
74+
if (input == null) {
75+
throw new IllegalArgumentException("Input file cannot be null.");
76+
}
77+
this.file = readToCleanUtf8Bytes(
78+
Files.newBufferedReader(input.toPath(), StandardCharsets.UTF_8)
79+
);
5680
}
5781

5882
/**
@@ -61,11 +85,39 @@ public BaseLocalResponse(File input) throws IOException {
6185
* @param input will be decoded as UTF-8.
6286
*/
6387
public BaseLocalResponse(Path input) throws IOException {
64-
this.file = this.getBytes(Files.lines(input, StandardCharsets.UTF_8));
88+
if (input == null) {
89+
throw new IllegalArgumentException("Input path cannot be null.");
90+
}
91+
this.file = readToCleanUtf8Bytes(Files.newBufferedReader(input, StandardCharsets.UTF_8));
6592
}
6693

67-
private byte[] getBytes(Stream<String> stream) {
68-
return stream.collect(Collectors.joining("")).getBytes();
94+
/**
95+
* Read, remove line endings, transform to UTF-8 bytes.
96+
*/
97+
private static byte[] readToCleanUtf8Bytes(Reader reader) {
98+
try (
99+
BufferedReader bufferedReader = (reader instanceof BufferedReader)
100+
? (BufferedReader) reader
101+
: new BufferedReader(reader)
102+
) {
103+
104+
StringBuilder stringBuilder = new StringBuilder();
105+
String line;
106+
107+
while ((line = bufferedReader.readLine()) != null) {
108+
stringBuilder.append(line);
109+
}
110+
111+
String cleanJson = stringBuilder.toString();
112+
113+
if (cleanJson.trim().isEmpty()) {
114+
throw new IllegalArgumentException("Input cannot be empty or contain only whitespace.");
115+
}
116+
117+
return cleanJson.getBytes(StandardCharsets.UTF_8);
118+
} catch (IOException e) {
119+
throw new UncheckedIOException("Failed to read input response payload.", e);
120+
}
69121
}
70122

71123
/**
@@ -84,7 +136,6 @@ public String getHmacSignature(String secretKey) {
84136
try {
85137
mac = Mac.getInstance(algorithm);
86138
} catch (NoSuchAlgorithmException err) {
87-
// this should never happen as the algorithm is hard-coded.
88139
return "";
89140
}
90141
try {
@@ -103,6 +154,22 @@ public String getHmacSignature(String secretKey) {
103154
* @return true if the signatures match.
104155
*/
105156
public boolean isValidHmacSignature(String secretKey, String signature) {
106-
return signature.equals(getHmacSignature(secretKey));
157+
if (signature == null || secretKey == null) {
158+
return false;
159+
}
160+
byte[] expectedBytes = getHmacSignature(secretKey).getBytes(StandardCharsets.UTF_8);
161+
byte[] actualBytes = signature
162+
.toLowerCase(java.util.Locale.ROOT)
163+
.getBytes(StandardCharsets.UTF_8);
164+
165+
return MessageDigest.isEqual(expectedBytes, actualBytes);
166+
}
167+
168+
/**
169+
* Print the file as a UTF-8 string.
170+
*/
171+
@Override
172+
public String toString() {
173+
return new String(this.file, StandardCharsets.UTF_8);
107174
}
108175
}

‎src/main/java/com/mindee/v1/parsing/LocalResponse.java‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
*/
1919
public class LocalResponse extends BaseLocalResponse {
2020

21-
public LocalResponse(InputStream input) {
21+
public LocalResponse(String input) {
2222
super(input);
2323
}
2424

25-
public LocalResponse(String input) {
25+
public LocalResponse(byte[] input) {
26+
super(input);
27+
}
28+
29+
public LocalResponse(InputStream input) {
2630
super(input);
2731
}
2832

‎src/main/java/com/mindee/v2/parsing/LocalResponse.java‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
*/
1515
public class LocalResponse extends BaseLocalResponse {
1616

17-
public LocalResponse(InputStream input) {
17+
public LocalResponse(String input) {
1818
super(input);
1919
}
2020

21-
public LocalResponse(String input) {
21+
public LocalResponse(byte[] input) {
22+
super(input);
23+
}
24+
25+
public LocalResponse(InputStream input) {
2226
super(input);
2327
}
2428

‎src/test/java/com/mindee/TestingUtilities.java‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,16 @@ public static Path getV1ResourcePath(String filePath) {
2828
return Paths.get("src/test/resources/v1/" + filePath);
2929
}
3030

31+
public static String getV1ResourcePathString(String filePath) {
32+
return getV1ResourcePath(filePath).toString();
33+
}
34+
3135
public static Path getV2ResourcePath(String filePath) {
3236
return Paths.get("src/test/resources/v2/" + filePath);
3337
}
3438

35-
public static String getV1ResourcePathString(String filePath) {
36-
return getV1ResourcePath(filePath).toString();
39+
public static Path getV2ProductPath(String filePath) {
40+
return getV2ResourcePath("products/" + filePath);
3741
}
3842

3943
public static void assertStringEqualsFile(String expected, String filePath) throws IOException {

‎src/test/java/com/mindee/v2/MindeeClientIT.java‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.mindee.v2;
22

33
import static com.mindee.TestingUtilities.getResourcePath;
4-
import static com.mindee.TestingUtilities.getV2ResourcePath;
4+
import static com.mindee.TestingUtilities.getV2ProductPath;
55
import static org.junit.jupiter.api.Assertions.*;
66

77
import com.mindee.input.LocalInputSource;
@@ -88,7 +88,7 @@ void parseFile_emptyMultiPage_mustSucceed() throws IOException, InterruptedExcep
8888
@DisplayName("Filled, single-page image – enqueue & parse must succeed")
8989
void parseFile_filledSinglePage_mustSucceed() throws IOException, InterruptedException {
9090
var source = new LocalInputSource(
91-
getV2ResourcePath("products/extraction/financial_document/default_sample.jpg")
91+
getV2ProductPath("extraction/financial_document/default_sample.jpg")
9292
);
9393

9494
var params = ExtractionParameters
@@ -137,16 +137,14 @@ void parseFile_filledSinglePage_mustSucceed() throws IOException, InterruptedExc
137137
@DisplayName("Data Schema Replace – enqueue & parse must succeed")
138138
void parseFile_dataSchemaReplace_mustSucceed() throws IOException, InterruptedException {
139139
var source = new LocalInputSource(
140-
getV2ResourcePath("products/extraction/financial_document/default_sample.jpg")
140+
getV2ProductPath("extraction/financial_document/default_sample.jpg")
141141
);
142142

143143
var params = ExtractionParameters
144144
.builder(modelId)
145145
.rag(false)
146146
.alias("java-integration-test_data-schema-replace")
147-
.dataSchema(
148-
Files.readString(getV2ResourcePath("products/extraction/data_schema_replace_param.json"))
149-
)
147+
.dataSchema(Files.readString(getV2ProductPath("extraction/data_schema_replace_param.json")))
150148
.build();
151149

152150
var response = mindeeClient.enqueueAndGetResult(ExtractionResponse.class, source, params);

‎src/test/java/com/mindee/v2/MindeeClientTest.java‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public <TResponse extends CommonResponse> TResponse reqGetResultByUrl(
9797
class Enqueue {
9898
@Test
9999
@DisplayName("sends exactly one HTTP call and yields a non-null response")
100-
void enqueue_post_async() throws IOException {
100+
void enqueue_post() throws IOException {
101101
var mindeeClient = new MindeeClient(new FakeMindeeApiV2(new JobResponse(), null));
102102

103103
var input = new LocalInputSource(getResourcePath("file_types/pdf/blank_1.pdf"));
@@ -115,7 +115,7 @@ void enqueue_post_async() throws IOException {
115115
class GetJob {
116116
@Test
117117
@DisplayName("hits the HTTP endpoint once and returns a non-null response")
118-
void document_getJob_async() throws JsonProcessingException {
118+
void document_getJob() throws JsonProcessingException {
119119
String json = "{\"job\": {\"id\": \"dummy-id\", \"status\": \"Processing\"}}";
120120
var mapper = new ObjectMapper();
121121
mapper.findAndRegisterModules();
@@ -134,7 +134,7 @@ void document_getJob_async() throws JsonProcessingException {
134134
class GetExtractionInference {
135135
@Test
136136
@DisplayName("hits the HTTP endpoint once and returns a non-null response")
137-
void document_getResult_async() throws IOException {
137+
void document_getResult() throws IOException {
138138
String json = Files
139139
.readString(getResourcePath("v2/products/extraction/financial_document/complete.json"));
140140

@@ -171,7 +171,7 @@ void document_getResult_async() throws IOException {
171171
class GetResultFromUrl {
172172
@Test
173173
@DisplayName("hits the HTTP endpoint once and returns a non-null response")
174-
void document_getResultFromUrl_async() throws IOException {
174+
void document_getResultFromUrl() throws IOException {
175175
String json = Files
176176
.readString(getResourcePath("v2/products/extraction/financial_document/complete.json"));
177177

‎src/test/java/com/mindee/v2/fileoperations/CropTest.java‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import static com.mindee.TestingUtilities.deleteRecursively;
44
import static com.mindee.TestingUtilities.getResourcePath;
5-
import static com.mindee.TestingUtilities.getV2ResourcePath;
5+
import static com.mindee.TestingUtilities.getV2ProductPath;
66
import static org.junit.jupiter.api.Assertions.assertEquals;
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88

@@ -26,8 +26,8 @@ public static void setup() throws IOException {
2626

2727
@Test
2828
void singlePageCrop_cropsCorrectly() throws Exception {
29-
var inputSample = new LocalInputSource(getV2ResourcePath("products/crop/default_sample.jpg"));
30-
var localResponse = new LocalResponse(getV2ResourcePath("products/crop/default_sample.json"));
29+
var inputSample = new LocalInputSource(getV2ProductPath("crop/default_sample.jpg"));
30+
var localResponse = new LocalResponse(getV2ProductPath("crop/default_sample.json"));
3131
var doc = localResponse.deserializeResponse(CropResponse.class);
3232

3333
var extractedCrops = new Crop(inputSample)
@@ -56,8 +56,8 @@ void singlePageCrop_cropsCorrectly() throws Exception {
5656

5757
@Test
5858
void multiPageCrop_cropsCorrectly() throws Exception {
59-
var inputSample = new LocalInputSource(getV2ResourcePath("products/crop/multipage_sample.pdf"));
60-
var localResponse = new LocalResponse(getV2ResourcePath("products/crop/multipage_sample.json"));
59+
var inputSample = new LocalInputSource(getV2ProductPath("crop/multipage_sample.pdf"));
60+
var localResponse = new LocalResponse(getV2ProductPath("crop/multipage_sample.json"));
6161
var doc = localResponse.deserializeResponse(CropResponse.class);
6262

6363
var extractedCrops = new Crop(inputSample)

‎src/test/java/com/mindee/v2/fileoperations/SplitTest.java‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import static com.mindee.TestingUtilities.deleteRecursively;
44
import static com.mindee.TestingUtilities.getResourcePath;
5-
import static com.mindee.TestingUtilities.getV2ResourcePath;
5+
import static com.mindee.TestingUtilities.getV2ProductPath;
66
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
77
import static org.junit.jupiter.api.Assertions.assertEquals;
88

@@ -26,9 +26,9 @@ public static void setup() throws IOException {
2626

2727
@Test
2828
void singlePage_splitsCorrectly() throws IOException {
29-
var inputSample = new LocalInputSource(getV2ResourcePath("products/split/default_sample.pdf"));
29+
var inputSample = new LocalInputSource(getV2ProductPath("split/default_sample.pdf"));
3030
assertEquals(2, inputSample.getPageCount());
31-
var localResponse = new LocalResponse(getV2ResourcePath("products/split/default_sample.json"));
31+
var localResponse = new LocalResponse(getV2ProductPath("split/default_sample.json"));
3232
var doc = localResponse.deserializeResponse(SplitResponse.class);
3333

3434
var extractedSplit = new Split(inputSample)
@@ -42,9 +42,9 @@ void singlePage_splitsCorrectly() throws IOException {
4242

4343
@Test
4444
void multiplePages_splitsCorrectly() throws IOException {
45-
var inputSample = new LocalInputSource(getV2ResourcePath("products/split/default_sample.pdf"));
45+
var inputSample = new LocalInputSource(getV2ProductPath("split/default_sample.pdf"));
4646
assertEquals(2, inputSample.getPageCount());
47-
var localResponse = new LocalResponse(getV2ResourcePath("products/split/default_sample.json"));
47+
var localResponse = new LocalResponse(getV2ProductPath("split/default_sample.json"));
4848
var doc = localResponse.deserializeResponse(SplitResponse.class);
4949

5050
var extractedSplits = new Split(inputSample)

0 commit comments

Comments
 (0)