Skip to content

Commit

Permalink
Add events service (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucko authored Mar 6, 2024
1 parent a35f475 commit c2ed958
Show file tree
Hide file tree
Showing 19 changed files with 938 additions and 0 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Test and Build

on:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'

jobs:
build-gradle:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: validate gradle wrapper
uses: gradle/wrapper-validation-action@v1

- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Gradle
uses: gradle/gradle-build-action@v2

- name: Run build and tests with Gradle wrapper
run: ./gradlew test build

- name: Publish test report
uses: mikepenz/action-junit-report@v3
if: success() || failure()
with:
report_paths: 'build/test-results/test/TEST-*.xml'
annotate_notice: true
detailed_summary: true
13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ plugins {
id 'maven-publish'
}

import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent

group = 'net.luckperms'
version = '0.1-SNAPSHOT'

Expand All @@ -21,6 +24,13 @@ repositories {

test {
useJUnitPlatform()
testLogging {
events = [TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED]
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
}
}

dependencies {
Expand All @@ -29,6 +39,9 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'com.squareup.okio:okio:1.17.5'
implementation 'com.google.code.gson:gson:2.9.1'
implementation('com.launchdarkly:okhttp-eventsource:4.1.1') {
exclude(module: 'okhttp')
}

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
Expand Down
172 changes: 172 additions & 0 deletions src/main/java/net/luckperms/rest/EventCallAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <[email protected]>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package net.luckperms.rest;

import com.google.gson.Gson;
import com.launchdarkly.eventsource.ConnectStrategy;
import com.launchdarkly.eventsource.EventSource;
import com.launchdarkly.eventsource.FaultEvent;
import com.launchdarkly.eventsource.MessageEvent;
import com.launchdarkly.eventsource.StreamEvent;
import net.luckperms.rest.event.EventCall;
import net.luckperms.rest.event.EventProducer;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.CallAdapter;

import java.lang.reflect.Type;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;

class EventCallAdapter implements CallAdapter<Object, EventCall<?>> {
private final Type eventType;
private final OkHttpClient client;
private final ExecutorService executorService;

EventCallAdapter(Type eventType, OkHttpClient client, ExecutorService executorService) {
this.eventType = eventType;
this.client = client;
this.executorService = executorService;
}

@Override
public Type responseType() {
return Object.class;
}

@Override
public EventCall<Object> adapt(Call<Object> call) {
return new EventCallImpl<>(call.request().url(), this.eventType, this.client, this.executorService);
}

private static final class EventCallImpl<E> implements EventCall<E> {
private final HttpUrl url;
private final Type eventType;
private final OkHttpClient client;
private final Executor executor;

EventCallImpl(HttpUrl url, Type eventType, OkHttpClient client, Executor executor) {
this.url = url;
this.eventType = eventType;
this.client = client;
this.executor = executor;
}

@Override
public EventProducer<E> subscribe() throws Exception {
EventSource eventSource = new EventSource.Builder(ConnectStrategy.http(this.url).httpClient(this.client)).build();
eventSource.start();

return new EventProducerImpl<>(eventSource, this.eventType, this.executor);
}
}

private static final class EventProducerImpl<E> implements EventProducer<E> {
private static final Gson GSON = new Gson();

private final EventSource eventSource;
private final Type eventType;

private final List<Consumer<E>> handlers;
private final List<Consumer<Exception>> errorHandlers;

private EventProducerImpl(EventSource eventSource, Type eventType, Executor executor) {
this.eventSource = eventSource;
this.eventType = eventType;
this.handlers = new CopyOnWriteArrayList<>();
this.errorHandlers = new CopyOnWriteArrayList<>();

executor.execute(this::pollForEvents);
}

private void pollForEvents() {
try {
for (StreamEvent event : this.eventSource.anyEvents()) {
if (event instanceof MessageEvent) {
handleMessage((MessageEvent) event);
} else if (event instanceof FaultEvent) {
handleError(((FaultEvent) event).getCause());
}
}
} catch (Exception e) {
handleError(e);
}
}

private void handleMessage(MessageEvent e) {
String eventName = e.getEventName();
if (!eventName.equals("message")) {
return;
}

E parsedEvent;
try {
parsedEvent = GSON.fromJson(e.getData(), this.eventType);
} catch (Exception ex) {
handleError(ex);
return;
}

for (Consumer<E> handler : this.handlers) {
try {
handler.accept(parsedEvent);
} catch (Exception ex) {
handleError(ex);
}
}
}

private void handleError(Exception e) {
for (Consumer<Exception> errorHandler : this.errorHandlers) {
try {
errorHandler.accept(e);
} catch (Exception ex) {
// ignore
}
}
}

@Override
public void subscribe(Consumer<E> consumer) {
this.handlers.add(consumer);
}

@Override
public void errorHandler(Consumer<Exception> errorHandler) {
this.errorHandlers.add(errorHandler);
}

@Override
public void close() {
this.eventSource.close();
}
}

}
66 changes: 66 additions & 0 deletions src/main/java/net/luckperms/rest/EventCallAdapterFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <[email protected]>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package net.luckperms.rest;

import net.luckperms.rest.event.EventCall;
import okhttp3.OkHttpClient;
import retrofit2.CallAdapter;
import retrofit2.Retrofit;

import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class EventCallAdapterFactory extends CallAdapter.Factory implements AutoCloseable {
private final OkHttpClient client;
private final ExecutorService executorService;

EventCallAdapterFactory(OkHttpClient client) {
this.client = client;
this.executorService = Executors.newCachedThreadPool();
}

@Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
if (getRawType(returnType) != EventCall.class) {
return null;
}

if (!(returnType instanceof ParameterizedType)) {
throw new IllegalArgumentException("Return type must be parameterized as EventCall<Foo> or EventCall<? extends Foo>");
}

Type responseType = getParameterUpperBound(0, (ParameterizedType) returnType);
return new EventCallAdapter(responseType, this.client, this.executorService);
}

@Override
public void close() {
this.executorService.shutdown();
}
}
8 changes: 8 additions & 0 deletions src/main/java/net/luckperms/rest/LuckPermsRestClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
package net.luckperms.rest;

import net.luckperms.rest.service.ActionService;
import net.luckperms.rest.service.EventService;
import net.luckperms.rest.service.GroupService;
import net.luckperms.rest.service.MiscService;
import net.luckperms.rest.service.TrackService;
Expand Down Expand Up @@ -75,6 +76,13 @@ static Builder builder() {
*/
ActionService actions();

/**
* Gets the event service.
*
* @return the event service
*/
EventService events();

/**
* Gets the misc service.
*
Expand Down
Loading

0 comments on commit c2ed958

Please sign in to comment.