Skip to content

Commit e56dea1

Browse files
author
Chris Hooks
committed
Refactored and fixed the attachment response handling
* Now attachments with binary data are supported
1 parent 60e7414 commit e56dea1

File tree

2 files changed

+122
-43
lines changed

2 files changed

+122
-43
lines changed

src/main/java/com/rusticisoftware/tincan/RemoteLRS.java

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ else if (req.getMethod() != "GET") {
250250

251251
for (HTTPPart part : req.getPartList()) {
252252
multiout.startPart(part.getContentType(), new String[]{
253-
"Content-Transfer-Encoding: binary",
254-
"X-Experience-API-Hash: " + part.getSha2()
253+
"Content-Transfer-Encoding: binary",
254+
"X-Experience-API-Hash: " + part.getSha2()
255255
});
256256
multiout.write(part.getContent());
257257
}
@@ -270,34 +270,31 @@ else if (req.getMethod() != "GET") {
270270
if (response.getContentType() != null && response.getContentType().contains("multipart/mixed")) {
271271
String boundary = response.getContentType().split("boundary=")[1];
272272

273-
MultipartParser responseHandler = new MultipartParser(listener.getContentAsString(), boundary);
274-
String temp = listener.getContentAsString();
275-
276-
// We need to get the first part that contains the statements and parse them
277-
responseHandler.nextPart();
278-
273+
MultipartParser responseHandler = new MultipartParser(listener.getContent(), boundary);
279274
ArrayList<Statement> statements = new ArrayList<Statement>();
280275

281-
if (responseHandler.getHeaders().get("Content-Type").contains("application/json")) {
282-
JsonNode statementsNode = (new StringOfJSON(new String(responseHandler.getContent())).toJSONNode());
283-
if (statementsNode.findPath("statements").isMissingNode()) {
284-
statements.add(new Statement(statementsNode));
276+
for (int i = 1; i < responseHandler.getSections().size(); i++) {
277+
responseHandler.parsePart(i);
278+
279+
if (i == 1) {
280+
if (responseHandler.getHeaders().get("Content-Type").contains("application/json")) {
281+
JsonNode statementsNode = (new StringOfJSON(new String(responseHandler.getContent())).toJSONNode());
282+
if (statementsNode.findPath("statements").isMissingNode()) {
283+
statements.add(new Statement(statementsNode));
284+
} else {
285+
statementsNode = statementsNode.findPath("statements");
286+
for (JsonNode obj : statementsNode) {
287+
statements.add(new Statement(obj));
288+
}
289+
}
290+
} else {
291+
throw new Exception("The first part of this response had a Content-Type other than \"application/json\"");
292+
}
285293
}
286294
else {
287-
statementsNode = statementsNode.findPath("statements");
288-
for (JsonNode obj : statementsNode) {
289-
statements.add(new Statement(obj));
290-
}
295+
response.setAttachment(responseHandler.getHeaders().get("X-Experience-API-Hash"), responseHandler.getContent());
291296
}
292297
}
293-
else {
294-
throw new Exception("The first part of this response had a Content-Type other than \"application/json\"");
295-
}
296-
297-
while (!responseHandler.noMoreParts) {
298-
responseHandler.nextPart();
299-
response.setAttachment(responseHandler.getHeaders().get("X-Experience-API-Hash"), responseHandler.getContent());
300-
}
301298
StatementsResult responseStatements = new StatementsResult();
302299
responseStatements.setStatements(statements);
303300
response.setContentBytes(responseStatements.toJSONNode(TCAPIVersion.V101).toString().getBytes());

src/main/java/com/rusticisoftware/tincan/internal/MultipartParser.java

Lines changed: 101 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,102 @@
2020
import lombok.Data;
2121
import org.eclipse.jetty.util.IO;
2222

23+
2324
@Data
2425
public class MultipartParser {
25-
private Scanner scanner;
26-
private String boundary = "";
27-
private HashMap<String, String> headers = new HashMap<String, String>();
26+
private byte[] boundary;
27+
private byte[] finalBoundary;
2828
private byte[] content;
29-
private String contentString;
30-
public boolean noMoreParts = false;
29+
private byte[] responseContent;
30+
private HashMap<String, String> headers = new HashMap<String, String>();
31+
private ArrayList<byte[]> sections = new ArrayList<byte[]>();
32+
private final byte[] CONTENT_DELIMITER = ("\r\n\r\n").getBytes();
33+
34+
public MultipartParser(byte[] responseContent, String boundary) throws IOException {
35+
this.responseContent = responseContent;
36+
this.boundary = ("--" + boundary).getBytes();
37+
this.finalBoundary = ("--" + boundary + "--").getBytes();
38+
splitIntoParts();
39+
}
40+
41+
/**
42+
* Code to find the index of a particular byte[] inside a larger byte[]
43+
* `indexOf` and `computeFailure` taken from StackOverflow user janko. Original response can be found at this link:
44+
* https://stackoverflow.com/questions/1507780/searching-for-a-sequence-of-bytes-in-a-binary-file-with-java
45+
*/
46+
private int indexOf(byte[] data, byte[] pattern) {
47+
int[] failure = computeFailure(pattern);
48+
49+
int j = 0;
50+
if (data.length == 0) return -1;
51+
52+
for (int i = 0; i < data.length; i++) {
53+
while (j > 0 && pattern[j] != data[i]) {
54+
j = failure[j - 1];
55+
}
56+
if (pattern[j] == data[i]) { j++; }
57+
if (j == pattern.length) {
58+
return i - pattern.length + 1;
59+
}
60+
}
61+
return -1;
62+
}
63+
64+
/**
65+
* Computes the failure function using a boot-strapping process,
66+
* where the pattern is matched against itself.
67+
*/
68+
private int[] computeFailure(byte[] pattern) {
69+
int[] failure = new int[pattern.length];
70+
71+
int j = 0;
72+
for (int i = 1; i < pattern.length; i++) {
73+
while (j > 0 && pattern[j] != pattern[i]) {
74+
j = failure[j - 1];
75+
}
76+
if (pattern[j] == pattern[i]) {
77+
j++;
78+
}
79+
failure[i] = j;
80+
}
81+
82+
return failure;
83+
}
84+
85+
private void splitIntoParts() {
86+
byte[] beforeFinalBoundary = Arrays.copyOfRange(responseContent, 0, indexOf(responseContent, finalBoundary));
87+
88+
int index;
89+
while ((index = indexOf(beforeFinalBoundary, boundary)) >= 0) {
90+
sections.add(Arrays.copyOfRange(beforeFinalBoundary, 0, index));
91+
beforeFinalBoundary = Arrays.copyOfRange(beforeFinalBoundary, index + this.boundary.length, beforeFinalBoundary.length);
92+
}
93+
while (beforeFinalBoundary[beforeFinalBoundary.length - 1] == (byte) '\n' || beforeFinalBoundary[beforeFinalBoundary.length - 1] == (byte) '\r') {
94+
beforeFinalBoundary = Arrays.copyOfRange(beforeFinalBoundary, 0, beforeFinalBoundary.length - 1);
95+
}
96+
sections.add(beforeFinalBoundary);
97+
}
3198

32-
public MultipartParser(String contentString, String boundary) throws IOException {
33-
this.contentString = contentString;
34-
this.scanner = new Scanner(contentString);
35-
this.boundary = "--" + boundary;
99+
private ArrayList<byte[]> splitDelimited(byte[] original, byte[] delimiter) {
100+
ArrayList<byte[]> parts = new ArrayList<byte[]>();
101+
102+
int index;
103+
while ((index = indexOf(original, delimiter)) >= 0) {
104+
parts.add(Arrays.copyOfRange(original, 0, index));
105+
original = Arrays.copyOfRange(original, index + delimiter.length, original.length);
106+
}
107+
while (original[original.length - 1] == (byte) '\n' || original[original.length - 1] == (byte) '\r') {
108+
original = Arrays.copyOfRange(original, 0, original.length - 1);
109+
}
110+
parts.add(original);
111+
112+
return parts;
36113
}
37114

38-
public void nextPart() throws IOException {
115+
public void parsePart(int i) throws IOException {
116+
ArrayList<byte[]> sectionParts = splitDelimited(sections.get(i), CONTENT_DELIMITER);
117+
Scanner scanner = new Scanner(new String(sectionParts.get(0)));
118+
39119
String line = scanner.nextLine();
40120

41121
// Check if the first line is "\r\n" or one of the headers
@@ -62,16 +142,18 @@ public void nextPart() throws IOException {
62142
}
63143
}
64144

65-
// Read in the content
66-
String contentString = "";
67-
while (scanner.hasNextLine() && !(line = scanner.nextLine()).equals(boundary)) {
68-
if (line.equals(boundary + "--")) {
69-
noMoreParts = true;
70-
break;
145+
if (headers.get("Content-Type").contains("application/json")) {
146+
scanner = new Scanner(new String(sectionParts.get(1)));
147+
148+
// Read in the content
149+
String contentString = "";
150+
while (scanner.hasNextLine() && !(line = scanner.nextLine()).equals(boundary)) {
151+
contentString += line;
71152
}
72-
contentString += line;
153+
content = contentString.getBytes();
154+
}
155+
else {
156+
content = sectionParts.get(1);
73157
}
74-
75-
content = contentString.getBytes();
76158
}
77159
}

0 commit comments

Comments
 (0)