Skip to content

Commit 217ec4d

Browse files
committed
First attempt on implementing SSE, doesn't work without the socket yet.
1 parent fab1dab commit 217ec4d

File tree

10 files changed

+118
-105
lines changed

10 files changed

+118
-105
lines changed

.github/linters/sun_checks.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
<module name="FileLength"/>
7474
<module name="LineLength">
7575
<property name="fileExtensions" value="java"/>
76-
<property name="max" value="120"/>
76+
<property name="max" value="150"/>
7777
</module>
7878

7979
<!-- Checks for whitespace -->

build.gradle

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ dependencies {
2525
implementation 'org.jetbrains:annotations:23.0.0'
2626
implementation 'io.tus.java.client:tus-java-client:0.4.5'
2727
implementation 'joda-time:joda-time:2.12.2'
28-
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
28+
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
2929
implementation 'org.json:json:20230227'
3030
implementation 'commons-codec:commons-codec:1.15'
3131
implementation 'io.socket:socket.io-client:2.1.0'
32+
implementation 'com.launchdarkly:okhttp-eventsource:4.1.1'
33+
3234

3335
testImplementation 'junit:junit:4.13.2'
3436
testImplementation 'org.mock-server:mockserver-junit-rule:5.15.0'

examples/src/main/java/com/transloadit/examples/async/AsyncExample.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public final class AsyncExample {
1616
* @param args
1717
*/
1818
public static void main(String[] args) {
19-
Transloadit transloadit = new Transloadit("TRANSLOADIT_KEY", "TRANSLOADIT_SECRET");
19+
Transloadit transloadit = new Transloadit(System.getenv("TRANSLOADIT_KEY"), System.getenv("TRANSLOADIT_SECRET"));
2020

2121
Map<String, Object> stepOptions = new HashMap<String, Object>();
2222
stepOptions.put("width", 75);

examples/src/main/java/com/transloadit/examples/async/AsyncPausePlayExample.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public final class AsyncPausePlayExample {
1616
* @param args
1717
*/
1818
public static void main(String[] args) {
19-
Transloadit transloadit = new Transloadit("TRANSLOADIT_KEY", "TRANSLOADIT_SECRET");
19+
Transloadit transloadit = new Transloadit(System.getenv("TRANSLOADIT_KEY"), System.getenv("TRANSLOADIT_SECRET"));
2020

2121
Map<String, Object> stepOptions = new HashMap<String, Object>();
2222
stepOptions.put("width", 75);

settings.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Uncomment following line if you want to use the local java-sdk
22
// for the example instead of pulling the JARs from JCenter.
33
// This is useful for debugging and testing new features.
4-
//include ':examples'
4+
include ':examples'
55
rootProject.name = 'transloadit'

src/main/java/com/transloadit/sdk/Assembly.java

+96-92
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
package com.transloadit.sdk;
22

3+
import com.launchdarkly.eventsource.ConnectStrategy;
4+
import com.launchdarkly.eventsource.EventSource;
5+
import com.launchdarkly.eventsource.MessageEvent;
6+
import com.launchdarkly.eventsource.background.BackgroundEventHandler;
7+
import com.launchdarkly.eventsource.background.BackgroundEventSource;
38
import com.transloadit.sdk.exceptions.LocalOperationException;
49
import com.transloadit.sdk.exceptions.RequestException;
510
import com.transloadit.sdk.response.AssemblyResponse;
6-
import io.socket.client.IO;
7-
import io.socket.client.Socket;
8-
import io.socket.emitter.Emitter;
9-
import io.socket.engineio.client.transports.WebSocket;
1011
import io.tus.java.client.ProtocolException;
1112
import io.tus.java.client.TusClient;
1213
import io.tus.java.client.TusURLMemoryStore;
1314
import io.tus.java.client.TusURLStore;
1415
import io.tus.java.client.TusUpload;
1516
import org.jetbrains.annotations.TestOnly;
17+
import org.json.JSONArray;
1618
import org.json.JSONObject;
1719

1820
import java.io.File;
1921
import java.io.FileNotFoundException;
2022
import java.io.IOException;
2123
import java.io.InputStream;
22-
import java.net.MalformedURLException;
23-
import java.net.URISyntaxException;
24+
import java.net.URI;
2425
import java.net.URL;
2526
import java.util.ArrayList;
2627
import java.util.HashMap;
@@ -29,6 +30,7 @@
2930
import java.util.concurrent.Executors;
3031
import java.util.concurrent.ThreadPoolExecutor;
3132
import java.util.UUID;
33+
import java.util.concurrent.TimeUnit;
3234
// CHECKSTYLE:OFF
3335
import io.tus.java.client.TusUploader;
3436
// CHECKTYLE:ON
@@ -48,7 +50,7 @@ public class Assembly extends OptionsBuilder {
4850
protected boolean shouldWaitForCompletion;
4951
protected AssemblyListener assemblyListener;
5052
protected AssemblyListener runnableAssemblyListener;
51-
protected Socket socket;
53+
protected BackgroundEventSource backgroundEventSource;
5254

5355

5456
protected ArrayList<TusUploadRunnable> threadList;
@@ -252,8 +254,8 @@ public AssemblyResponse save(boolean isResumable)
252254
throw new RequestException("Request to Assembly failed: " + response.json().getString("error"));
253255
}
254256

255-
if (shouldWaitWithSocket()) {
256-
listenToSocket(response);
257+
if (shouldWaitWithSSE()) {
258+
listenToServerSentEvents(response);
257259
}
258260

259261
try {
@@ -265,12 +267,12 @@ public AssemblyResponse save(boolean isResumable)
265267
}
266268
} else {
267269
response = new AssemblyResponse(request.post(obtainUploadUrlSuffix(), options, null, files, fileStreams));
268-
if (shouldWaitWithSocket() && !response.isFinished()) {
269-
listenToSocket(response);
270+
if (shouldWaitWithSSE() && !response.isFinished()) {
271+
listenToServerSentEvents(response);
270272
}
271273
}
272274

273-
return shouldWaitWithoutSocket() ? waitTillComplete(response) : response;
275+
return shouldWaitWithoutSSE() ? waitTillComplete(response) : response;
274276
}
275277

276278
/**
@@ -498,115 +500,117 @@ public void onAssemblyResultFinished(String stepName, JSONObject result) {
498500
* <li>{@code false} if the client should not wait for completion by observing the HTTP - Response</li></ul>
499501
* @see Assembly#save(boolean) Usage in Assembly.save()
500502
*/
501-
protected boolean shouldWaitWithoutSocket() {
503+
protected boolean shouldWaitWithoutSSE() {
502504
return this.shouldWaitForCompletion && this.assemblyListener == null;
503505
}
504506

505507
/**
506-
* Determines if the Client should wait until the Assembly execution is finished by observing a server socket. <p>
508+
* Determines if the Client should wait until the Assembly execution is finished by observing a server sent events (SSE). <p>
507509
* Can only be {@code true} if <code> {@link #shouldWaitForCompletion} = true</code> and an
508510
* {@link AssemblyListener} has been specified.</p>
509-
* @return <ul><li>{@code true} if the client should wait for Assembly completion by observing the socket</li>
510-
* <li>{@code false} if the client should not wait for completion by observing the socket.</li></ul>
511+
* @return <ul><li>{@code true} if the client should wait for Assembly completion by observing SSE</li>
512+
* <li>{@code false} if the client should not wait for completion by observing the SSE.</li></ul>
511513
* @see Assembly#save(boolean) Usage in Assembly.save()
512514
*/
513-
protected boolean shouldWaitWithSocket() {
515+
protected boolean shouldWaitWithSSE() {
514516
return this.shouldWaitForCompletion && this.assemblyListener != null;
515517
}
516518

517-
/**
518-
* Opens a Websocket to the provided URL in order to receive updates on the assembly's execution status.
519-
* @param socketUrl target url to open the WebSocket at.
520-
* @return {@link Socket}
521-
* @throws LocalOperationException
522-
*/
523-
Socket getSocket(String socketUrl) throws LocalOperationException {
524-
IO.Options options = new IO.Options();
525-
options.transports = new String[] {WebSocket.NAME };
526-
try {
527-
URL url = new URL(socketUrl);
528-
options.path = url.getPath();
529-
String host = url.getProtocol() + "://" + url.getHost();
530-
return IO.socket(host, options);
531-
} catch (URISyntaxException | MalformedURLException e) {
532-
throw new LocalOperationException(e);
533-
}
534-
}
535-
536519
/**
537520
* Wait till the assembly is finished and then return the response of the complete state.
538521
*
539522
* @param response {@link AssemblyResponse}
540-
* @throws LocalOperationException if something goes wrong while running non-http operations.
541523
*/
542-
private void listenToSocket(AssemblyResponse response) throws LocalOperationException {
543-
final String assemblyUrl = response.getSslUrl();
544-
final String assemblyId = response.getId();
524+
private void listenToServerSentEvents(AssemblyResponse response) {
525+
final URI sseUpdateStreamUrl = URI.create(response.getUpdateStreamUrl());
526+
545527

546-
socket = getSocket(response.getWebsocketUrl());
547-
Emitter.Listener onFinished = new Emitter.Listener() {
528+
BackgroundEventHandler myHandler = new BackgroundEventHandler() {
548529
@Override
549-
public void call(Object... args) {
550-
socket.disconnect();
551-
try {
552-
getAssemblyListener().onAssemblyFinished(transloadit.getAssemblyByUrl(assemblyUrl));
553-
} catch (RequestException e) {
554-
getAssemblyListener().onError(e);
555-
} catch (LocalOperationException e) {
556-
getAssemblyListener().onError(e);
557-
}
530+
public void onOpen() {
558531
}
559-
};
560532

561-
Emitter.Listener onConnect = new Emitter.Listener() {
562533
@Override
563-
public void call(Object... args) {
564-
JSONObject obj = new JSONObject();
565-
obj.put("id", assemblyId);
566-
socket.emit("assembly_connect", obj);
534+
public void onClosed() {
535+
}
536+
// Here the different SSE sent events are getting piped to the corresponding assembly event via the {@link AssemblyListener}
537+
public void onMessage(String event, MessageEvent messageEvent) {
538+
if (event != null && messageEvent != null) {
539+
// In case of a message event, without additional payload.
540+
if (event.equals("message")) {
541+
String messageContent = messageEvent.getData();
542+
if (messageContent.equals("assembly_finished")) {
543+
try {
544+
getAssemblyListener().onAssemblyFinished(transloadit.getAssemblyByUrl(response.getSslUrl()));
545+
} catch (RequestException | LocalOperationException e) {
546+
getAssemblyListener().onError(e);
547+
} finally {
548+
// Close the event source, as the assembly encoding process has finished.
549+
getBackgroundEventSource().close();
550+
}
551+
}
552+
553+
if (messageContent.equals("assembly_uploading_finished")) {
554+
getAssemblyListener().onAssemblyUploadFinished();
555+
}
556+
557+
if (messageContent.equals("assembly_upload_meta_data_extracted")) {
558+
getAssemblyListener().onMetadataExtracted();
559+
}
560+
561+
// In case of regular events, which are coming with extra payloads from the server.
562+
} else {
563+
// Some events are not wrapped inside a plain JSON Object, but inside a JSON array.
564+
JSONArray messageEventArray = new JSONArray(messageEvent.getData());
565+
566+
if (event.equals("assembly_result_finished")) {
567+
// Unpack the two expected fields in the JSON array.
568+
String stepName = messageEventArray.getString(0);
569+
JSONObject messageEventJson = messageEventArray.getJSONObject(1);
570+
getAssemblyListener().onAssemblyResultFinished(stepName, messageEventJson);
571+
}
572+
573+
if (event.equals("assembly_error")) {
574+
// Deliver error information to the user.
575+
JSONObject messageEventJson = messageEventArray.getJSONObject(0);
576+
String errorString = messageEventJson.getString("error") + "\n" + messageEventJson.toString(2);
577+
getAssemblyListener().onError(new RequestException(errorString));
578+
}
579+
580+
if (event.equals("assembly_upload_finished")) {
581+
JSONObject messageEventJson = messageEventArray.getJSONObject(0);
582+
getAssemblyListener().onFileUploadFinished(messageEventJson.getString("name"), messageEventJson);
583+
}
584+
}
585+
}
567586
}
568-
};
569587

570-
Emitter.Listener onError = new Emitter.Listener() {
571588
@Override
572-
public void call(Object... args) {
573-
socket.disconnect();
574-
getAssemblyListener().onError((Exception) args[0]);
589+
public void onComment(String comment) {
575590
}
576-
};
577591

578-
Emitter.Listener onMetadataExtracted = args -> {
579-
getAssemblyListener().onMetadataExtracted();
580-
};
592+
@Override
593+
public void onError(Throwable t) {
594+
}
581595

582-
Emitter.Listener onAssemblyResultFinished = args -> {
583-
String stepName = (String) args[0];
584-
JSONObject result = (JSONObject) args[1];
585-
getAssemblyListener().onAssemblyResultFinished(stepName, result);
586596
};
597+
this.backgroundEventSource = new BackgroundEventSource.Builder(myHandler, new EventSource.Builder(
598+
ConnectStrategy.http(sseUpdateStreamUrl)
599+
.header("Accept", "text/event-stream")
600+
.connectTimeout(5, TimeUnit.SECONDS)
601+
)
602+
).build();
587603

588-
//Hands over Filename of recently uploaded file to the callback in the AssemblyListener
589-
Emitter.Listener onFileUploadFinished = args -> {
590-
String name = ((JSONObject) args[0]).getString("name");
591-
JSONObject uploadInformation = (JSONObject) args[0];
592-
getAssemblyListener().onFileUploadFinished(name, uploadInformation);
593-
};
604+
this.backgroundEventSource.start();
594605

595-
// Triggers callback in the {@link Assembly#assemblyListener} if the Assembly instructions have been uploaded.
596-
Emitter.Listener onAssemblyUploadFinished = args -> {
597-
getAssemblyListener().onAssemblyUploadFinished();
598-
};
606+
}
599607

600-
socket
601-
.on(Socket.EVENT_CONNECT, onConnect)
602-
.on("assembly_finished", onFinished)
603-
.on("assembly_uploading_finished", onAssemblyUploadFinished)
604-
.on("assembly_upload_finished", onFileUploadFinished)
605-
.on("assembly_upload_meta_data_extracted", onMetadataExtracted)
606-
.on("assembly_result_finished", onAssemblyResultFinished)
607-
.on("assembly_error", onFinished)
608-
.on(Socket.EVENT_CONNECT_ERROR, onError);
609-
socket.connect();
608+
/**
609+
* Returns the Event Source, which handles the Server Sent Event driven assembly status updates.
610+
* @return BackgroundEventSource, which handles the assembly Status via SSE.
611+
*/
612+
public BackgroundEventSource getBackgroundEventSource() {
613+
return backgroundEventSource;
610614
}
611615

612616
/**
@@ -733,8 +737,8 @@ protected void abortUploads(Exception e) {
733737
executor.shutdownNow();
734738
}
735739
runnableAssemblyListener.onError(e);
736-
if (socket != null) {
737-
socket.disconnect();
740+
if (backgroundEventSource != null) {
741+
backgroundEventSource.close();
738742
}
739743
}
740744

src/main/java/com/transloadit/sdk/async/AsyncAssembly.java

+4-6
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ synchronized void setState(State state) {
160160
* Returns always false to indicate to the {@link Assembly#save} method that it should never wait for the Assembly
161161
* to be complete by observing the HTTP - Response.
162162
* @return false
163-
* @see Assembly#shouldWaitWithoutSocket()
163+
* @see Assembly#shouldWaitWithoutSSE()
164164
* @see Assembly#save(boolean)
165165
*/
166-
protected boolean shouldWaitWithoutSocket() {
166+
protected boolean shouldWaitWithoutSSE() {
167167
return false;
168168
}
169169

@@ -279,12 +279,10 @@ public void run() {
279279

280280
if (state == State.UPLOAD_COMPLETE) {
281281
getUploadListener().onUploadFinished();
282-
if (!shouldWaitWithSocket() && shouldWaitForCompletion && (getListener() != null)) {
282+
if (!shouldWaitWithSSE() && shouldWaitForCompletion && (getListener() != null)) {
283283
try {
284284
getListener().onAssemblyFinished(watchStatus());
285-
} catch (LocalOperationException e) {
286-
getListener().onAssemblyStatusUpdateFailed(e);
287-
} catch (RequestException e) {
285+
} catch (LocalOperationException | RequestException e) {
288286
getListener().onAssemblyStatusUpdateFailed(e);
289287
} finally {
290288
executor.stop();

src/main/java/com/transloadit/sdk/response/AssemblyResponse.java

+8
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ public String getSslUrl() {
5959
return this.json().getString("assembly_ssl_url");
6060
}
6161

62+
/**
63+
* Retruns the upstream url needed for Server Sent Events.
64+
* @return upstream url
65+
*/
66+
public String getUpdateStreamUrl() {
67+
return this.json().getString("update_stream_url");
68+
}
69+
6270
/**
6371
* Returns the URL of the websocket used in the Assembly execution.
6472
* @return assembly websocket url

src/test/java/com/transloadit/sdk/AssemblyTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ public void saveWithInputStream() throws Exception {
169169
* setting {@link Assembly#shouldWaitForCompletion} = {@code true}.
170170
* @throws Exception if communication with the server goes wrong, if building the request goes wrong or
171171
* if Test resources "assembly_executing.json" or "resumable_assembly_complete.json" are missing.
172-
* @see Assembly#shouldWaitWithoutSocket()
172+
* @see Assembly#shouldWaitWithoutSSE()
173173
*/
174174
@Test
175175
public void saveTillComplete() throws Exception {
@@ -303,7 +303,7 @@ public void onAssemblyResultFinished(String stepName, JSONObject result) {
303303
* This Test verifies the functionality of {@link Assembly#save(boolean)}. It is identical to
304304
* {@link AssemblyTest#saveWithTus()}, except it waits until the {@link Assembly} execution is finished.
305305
* This is determined by by observing the {@link AssemblyResponse} status.
306-
* @see Assembly#shouldWaitWithoutSocket()
306+
* @see Assembly#shouldWaitWithoutSSE()
307307
* @throws Exception if communication with the server goes wrong, if building the request goes wrong or
308308
* if Test resources "resumable_assembly.json" or "resumable_assembly_complete.json" are missing.
309309
*/

0 commit comments

Comments
 (0)