Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build with Maven
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<version>1.1.3</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>

<dependencies>
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/swofty/redisapi/api/RedisAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import net.swofty.redisapi.api.requests.DataStreamListener;
import net.swofty.redisapi.events.EventRegistry;
import net.swofty.redisapi.events.RedisMessagingReceiveEvent;
import net.swofty.redisapi.events.RedisMessagingReceiveInterface;
Expand Down Expand Up @@ -141,6 +142,13 @@ public static RedisAPI generateInstance(@NonNull String uri, String password) {
* Starts listeners for the Redis Pub/Sub channels
*/
public void startListeners() {
try {
registerChannel("internal-data-request", DataStreamListener.class);
} catch (ChannelAlreadyRegisteredException ignored) {
System.out.println("[WARNING]: The internal data request channel has already been registered. This will cause issues if you are using the DataRequest API along with the Redis API." +
"\n Channel Name: internal-data-request");
}

new Thread(() -> {
try (Jedis jedis = getPool().getResource()) {
EventRegistry.pubSub = new JedisPubSub() {
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/net/swofty/redisapi/api/requests/DataRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package net.swofty.redisapi.api.requests;

import net.swofty.redisapi.api.ChannelRegistry;
import net.swofty.redisapi.api.RedisAPI;
import net.swofty.redisapi.util.RedisParsableMessage;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

public class DataRequest {
public static final Map<String, JSONObject> RECEIVED_DATA = new HashMap<>();

private final String id;
private final String filter;
private final String key;
private final JSONObject data;

/**
* Create a new data request to get a specific object of data from a specific filter ID.
* @param filterID The filter ID where you want to receive data from, can be "all" for all listeners.
* @param key The data identifier key.
*/
public DataRequest(String filterID, String key, JSONObject data) {
this.id = UUID.randomUUID().toString();
this.filter = filterID;
this.key = key;
this.data = data;
}

public CompletableFuture<DataResponse> await() {
return CompletableFuture.supplyAsync(() -> {
long start = System.currentTimeMillis();
JSONObject request = new JSONObject();
request.put("id", id);
request.put("key", key);
request.put("data", data);
request.put("sender", "proxy");
request.put("stream", StreamType.REQUEST.name());

RedisAPI.getInstance().publishMessage(filter, ChannelRegistry.getFromName("internal-data-request"), RedisParsableMessage.from(request).formatForSend());

int timeout = 0;
while (!RECEIVED_DATA.containsKey(id)) {
try { Thread.sleep(1); timeout++; } catch (InterruptedException ignored) { }
if (timeout >= 100) break;
}

JSONObject response = RECEIVED_DATA.get(id);
RECEIVED_DATA.remove(id);
long latency = (System.currentTimeMillis() - start);
return new DataResponse(response, latency);
});
}

public enum StreamType {
REQUEST,
RESPONSE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package net.swofty.redisapi.api.requests;

import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

public class DataRequestResponder {
public static final Map<String, DataRequestResponder> RESPONDERS = new HashMap<>();

private final Function<JSONObject, JSONObject> callback;

protected DataRequestResponder(Function<JSONObject, JSONObject> callback) {
this.callback = callback;
}

public JSONObject respond(JSONObject request) {
return this.callback.apply(request);
}

/**
* Creates a new Data Request Responder. Must be registered before {@link net.swofty.redisapi.api.RedisAPI#startListeners()} in order to work properly.
* @param key The key to respond to.
* @param callback Callback, has a JSONObject parameter request, and returns a JSONObject response, request and response both can be empty.
* @return The created DataRequestResponder, not entirely useful, but still there.
*/
public static DataRequestResponder create(String key, Function<JSONObject, JSONObject> callback) {
DataRequestResponder responder = new DataRequestResponder(callback);
RESPONDERS.put(key, responder);
return responder;
}

/**
* Get a DataRequestResponder by key.
* @param key The key to get the DataRequestResponder by.
* @return The DataRequestResponder, or null if it doesn't exist.
*/
public static DataRequestResponder get(String key) {
return RESPONDERS.get(key);
}
}
11 changes: 11 additions & 0 deletions src/main/java/net/swofty/redisapi/api/requests/DataResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.swofty.redisapi.api.requests;

import org.json.JSONObject;

/**
* The response to a DataRequest.
* @param data The data object, will be null if the request has timed out.
* @param latency The latency of the request, normal range is between 1-10ms, unless the server is under heavy load or the Redis server is running on a different machine.
*/
public record DataResponse(JSONObject data, long latency) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package net.swofty.redisapi.api.requests;

import net.swofty.redisapi.api.ChannelRegistry;
import net.swofty.redisapi.api.RedisAPI;
import net.swofty.redisapi.events.RedisMessagingReceiveInterface;
import net.swofty.redisapi.util.RedisParsableMessage;
import org.json.JSONObject;

public class DataStreamListener implements RedisMessagingReceiveInterface {
@Override
public void onMessage(String channel, String message) {
RedisParsableMessage msg = RedisParsableMessage.parse(message);
DataRequest.StreamType type = DataRequest.StreamType.valueOf(msg.get("stream", "NONE"));
String key = msg.get("key", "NONE");
String id = msg.get("id", "NONE");
String sender = msg.get("sender", "NONE");
JSONObject data = msg.getJson().getJSONObject("data");

switch (type) {
case REQUEST -> {
DataRequestResponder responder = DataRequestResponder.get(key);
if (responder == null) return;

JSONObject response = responder.respond(data);
JSONObject responseJson = new JSONObject();
responseJson.put("id", id);
responseJson.put("sender", "internal");
responseJson.put("stream", DataRequest.StreamType.RESPONSE.name());
responseJson.put("key", key);
responseJson.put("data", response);

RedisAPI.getInstance().publishMessage(sender, ChannelRegistry.getFromName("internal-data-request"),
RedisParsableMessage.from(responseJson).formatForSend());
}
case RESPONSE -> DataRequest.RECEIVED_DATA.put(id, data);
}
}
}
104 changes: 104 additions & 0 deletions src/main/java/net/swofty/redisapi/util/RedisParsableMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package net.swofty.redisapi.util;

import lombok.Getter;
import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
* This is a utility class, used for sending JSONObjects over Redis instead of working with raw Strings.
*/
@Getter
public class RedisParsableMessage {
private final JSONObject json;

protected RedisParsableMessage(JSONObject json) {
this.json = json;
}

/**
* Builds a new RedisParsableMessage from a JSONObject.
* @param fields The fields to build the JSONObject from.
* @return The built RedisParsableMessage.
*/
public static RedisParsableMessage from( Map<String, Object> fields) {
return from(new JSONObject(fields));
}

/**
* Builds a new RedisParsableMessage from a JSONObject.
* @param obj The JSONObject to build the RedisParsableMessage from.
* @return The built RedisParsableMessage.
*/
public static RedisParsableMessage from(JSONObject obj) {
return new RedisParsableMessage(obj);
}

/**
* Parse a RedisParsableMessage from a raw String.
* @param raw The raw String to parse.
* @return The parsed RedisParsableMessage.
* @throws IllegalArgumentException if the raw String is not a valid JSONObject.
*/
public static RedisParsableMessage parse(String raw) {
String toParse = raw;
if (raw.contains(";")) {
String[] split = raw.split(";");
toParse = split[1];
}
return new RedisParsableMessage(new JSONObject(toParse));
}

/**
* Formats the JSONObject into a String to send over Redis, this is the same as {@link #json#toString()}.
* @return The formatted String.
*/
public String formatForSend() {
return json.toString();
}

@Override
public String toString() {
return formatForSend();
}

/**
* Get an object from the JSONObject.
* @param key The key to get the object from.
* @param defaultValue The default value to return if the key is not found.
* @return The object.
* @param <T> The type of the object.
*/
public <T> T get(String key, T defaultValue) {
return json.has(key) ? (T) json.get(key) : defaultValue;
}

/*
* Beyond here are some utility methods for getting data from the JSONObject.
*/

/**
* Get a UUID from the JSONObject.
* @param key The key to get the UUID from.
* @return The UUID.
*/
public UUID getUUID(String key) {
return UUID.fromString(get(key, ""));
}

public JSONArray getJsonArray(String key) {
return json.has(key) ? json.getJSONArray(key) : new JSONArray();
}

public List<String> getStringList(String key) {
return json.has(key) ? json.getJSONArray(key).toList().stream().map(String::valueOf).toList() : new ArrayList<>();
}

public boolean getBoolean(String key) {
return json.has(key) && json.getBoolean(key);
}
}
Loading