Merge branch 'feat/serverside-response' into 'main'

Create components to respond to client request

Closes #8

See merge request cs108-fs26/Gruppe-13!44
This commit was merged in pull request #200.
This commit is contained in:
Lars Simon Winzer
2026-03-24 15:02:16 +01:00
12 changed files with 421 additions and 0 deletions
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Response representing an error outcome for a client's request. */
public class ErrorResponse extends Response {
/**
* Construct an error response with a code and message.
*
* @param sessionId the target session id
* @param requestId the originating request id
* @param errorCode a short error code identifying the failure
* @param errorMessage a human readable error message
*/
public ErrorResponse(
SessionId sessionId, int requestId, String errorCode, String errorMessage) {
super(
sessionId,
requestId,
ResponseBody.builder().param("CODE", errorCode).param("MSG", errorMessage).build());
}
/**
* {@inheritDoc}
*
* <p>This implementation returns the fixed {@code -ERR} prefix.
*
* @return the {@code -ERR} prefix
*/
@Override
public String prefix() {
return "-ERR";
}
}
@@ -0,0 +1,20 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* A simple success response with an empty body.
*
* <p>Use this to acknowledge successful requests that do not carry additional payload data.
*/
public class OkResponse extends SuccessResponse {
/**
* Create a minimal successful response (no body content).
*
* @param sessionId the target session id
* @param requestId the originating request id
*/
public OkResponse(SessionId sessionId, int requestId) {
super(sessionId, requestId, ResponseBody.builder().build());
}
}
@@ -2,4 +2,11 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* Immutable transport record representing an encoded response ready for delivery to a session.
*
* @param sessionId the target session id
* @param requestId the originating request id
* @param payload the serialized response payload
*/
public record PrimitiveResponse(SessionId sessionId, int requestId, String payload) {} public record PrimitiveResponse(SessionId sessionId, int requestId, String payload) {}
@@ -0,0 +1,58 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/** Abstract base class for all server responses sent to clients. */
public abstract class Response {
private final SessionId sessionId;
private final int requestId;
private final ResponseBody body;
/**
* Create a new {@code Response}.
*
* @param sessionId the id of the session this response targets
* @param requestId the request identifier this response corresponds to
* @param body the structured response body
*/
protected Response(SessionId sessionId, int requestId, ResponseBody body) {
this.sessionId = sessionId;
this.requestId = requestId;
this.body = body;
}
/**
* Returns the protocol prefix for this response (for example {@code "+OK"} or {@code "-ERR"}).
*
* @return the response prefix string used by the encoder
*/
public abstract String prefix();
/**
* Returns the session id that should receive this response.
*
* @return the target {@link SessionId}
*/
public SessionId getSessionId() {
return sessionId;
}
/**
* Returns the request identifier associated with this response.
*
* @return the numeric request id
*/
public int getRequestId() {
return requestId;
}
/**
* Returns the immutable {@link ResponseBody} that carries the structured payload for this
* response.
*
* @return the response body
*/
public ResponseBody getBody() {
return body;
}
}
@@ -0,0 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.List;
/**
* A block node that contains a tag and a list of child {@link ResponseNode} elements. Blocks can be
* nested to build hierarchical response bodies.
*
* @param tag the block tag
* @param children the child nodes contained in this block
*/
public record ResponseBlock(String tag, List<ResponseNode> children) implements ResponseNode {}
@@ -0,0 +1,40 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.List;
/**
* Immutable container for the structured content of a {@link Response}.
*
* <p>A {@code ResponseBody} holds an ordered list of {@link ResponseNode} items (parameters and
* blocks). Use {@link #builder()} to construct instances.
*/
public class ResponseBody {
private final List<ResponseNode> nodes;
/**
* Package-private constructor used by {@link ResponseBodyBuilder}.
*
* @param nodes the list of response nodes to include in this body
*/
ResponseBody(List<ResponseNode> nodes) {
this.nodes = List.copyOf(nodes);
}
/**
* Create a new {@link ResponseBodyBuilder} for assembling a response body.
*
* @return a fresh builder instance
*/
public static ResponseBodyBuilder builder() {
return new ResponseBodyBuilder();
}
/**
* Returns the ordered list of {@link ResponseNode} elements contained in this body.
*
* @return an immutable list of nodes
*/
public List<ResponseNode> nodes() {
return nodes;
}
}
@@ -0,0 +1,51 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* Builder for {@link ResponseBody} instances.
*
* <p>Provides methods to append parameter nodes and nested blocks and to produce an immutable
* {@link ResponseBody} via {@link #build()}.
*/
public class ResponseBodyBuilder {
private final List<ResponseNode> nodes = new ArrayList<>();
/**
* Add a key/value parameter to the response body under construction.
*
* @param key the parameter name
* @param value the parameter value (will be converted to string when encoded)
* @return this builder for fluent chaining
*/
public ResponseBodyBuilder param(String key, Object value) {
nodes.add(new ResponseParameter(key, value));
return this;
}
/**
* Add a nested block with the given tag. The provided consumer receives a child builder to
* populate the block content.
*
* @param tag the block tag
* @param content consumer that appends child nodes to the block
* @return this builder for fluent chaining
*/
public ResponseBodyBuilder block(String tag, Consumer<ResponseBodyBuilder> content) {
ResponseBodyBuilder childBuilder = new ResponseBodyBuilder();
content.accept(childBuilder);
nodes.add(new ResponseBlock(tag, childBuilder.build().nodes()));
return this;
}
/**
* Build an immutable {@link ResponseBody} from the accumulated nodes.
*
* @return a new {@link ResponseBody}
*/
public ResponseBody build() {
return new ResponseBody(nodes);
}
}
@@ -0,0 +1,36 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
/**
* Helper that dispatches {@link Response} instances to the corresponding {@link Session} by
* encoding them and enqueuing the resulting {@link PrimitiveResponse} into the session's response
* queue.
*/
public class ResponseDispatcher {
private final SessionManager sessionManager;
/**
* Create a dispatcher bound to a {@link SessionManager}.
*
* @param sessionManager manager used to resolve sessions
*/
public ResponseDispatcher(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
/**
* Encode the given {@link Response} and enqueue the resulting {@link PrimitiveResponse} into
* the target session's response queue.
*
* @param response the response to dispatch
* @throws InterruptedException if the thread is interrupted while waiting to enqueue the
* primitive response
*/
public void dispatch(Response response) throws InterruptedException {
PrimitiveResponse primitiveResponse = ResponseEncoder.encode(response);
Session session = sessionManager.getSessionById(response.getSessionId());
session.getResponseQueue().put(primitiveResponse);
}
}
@@ -0,0 +1,98 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* Utility responsible for encoding a {@link Response} into a protocol payload string and wrapping
* it into a {@link PrimitiveResponse} suitable for transmission.
*/
public class ResponseEncoder {
private static final String INDENT = "\t";
private static final String NEWLINE = "\n";
/**
* Encode a {@link Response} into a {@link PrimitiveResponse} containing the serialized payload
* string.
*
* @param response the response to encode
* @return a {@link PrimitiveResponse} with encoded payload
*/
public static PrimitiveResponse encode(Response response) {
StringBuilder sb = new StringBuilder();
sb.append(response.prefix());
for (ResponseNode node : response.getBody().nodes()) {
sb.append(NEWLINE);
encodeNode(node, sb, 1);
}
sb.append(NEWLINE).append("END");
return new PrimitiveResponse(
response.getSessionId(), response.getRequestId(), sb.toString());
}
/**
* Internal helper to encode any {@link ResponseNode}.
*
* @param node node to encode
* @param sb string builder to append to
* @param depth current indentation depth
*/
private static void encodeNode(ResponseNode node, StringBuilder sb, int depth) {
if (node instanceof ResponseParameter param) {
encodeParameter(param, sb, depth);
} else if (node instanceof ResponseBlock block) {
encodeBlock(block, sb, depth);
}
}
/**
* Encode a {@link ResponseParameter} into the string builder.
*
* @param param the parameter to encode
* @param sb the output builder
* @param depth indentation depth
*/
private static void encodeParameter(ResponseParameter param, StringBuilder sb, int depth) {
sb.append(INDENT.repeat(depth));
sb.append(param.key());
sb.append("=");
sb.append(maskIfNeeded(param.value().toString()));
}
/**
* Encode a {@link ResponseBlock}, including its children and terminating with an {@code END}
* marker.
*
* @param block the block to encode
* @param sb the output builder
* @param depth current indentation depth
*/
private static void encodeBlock(ResponseBlock block, StringBuilder sb, int depth) {
sb.append(INDENT.repeat(depth));
sb.append(block.tag());
for (ResponseNode child : block.children()) {
sb.append(NEWLINE);
encodeNode(child, sb, depth + 1);
}
sb.append(NEWLINE);
sb.append(INDENT.repeat(depth));
sb.append("END");
}
/**
* Quote or escape the provided value if it contains whitespace or single quotes so the encoded
* payload remains parseable.
*
* @param value the raw string value
* @return quoted/escaped value
*/
private static String maskIfNeeded(String value) {
if (value.contains(" ") || value.contains("'")) {
String escaped = value.replace("'", "\\'");
return "'" + escaped + "'";
}
return value;
}
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* Marker interface for elements that may appear in a {@link ResponseBody}.
*
* <p>Implementations include {@link ResponseParameter} and {@link ResponseBlock}.
*/
public interface ResponseNode {}
@@ -0,0 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
/**
* A parameter node stored in a {@link ResponseBody}.
*
* <p>Represents a simple key/value pair. Callers can use {@link #rawValue()} to obtain the string
* representation of the stored value.
*
* @param key the parameter name
* @param value the parameter value
*/
public record ResponseParameter(String key, Object value) implements ResponseNode {
/**
* Returns the raw string representation of the stored value. This is a convenience wrapper
* around {@code Object#toString()} and may throw {@link NullPointerException} if the stored
* value is {@code null}.
*
* @return the value as string
*/
public String rawValue() {
return value.toString();
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.response;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
/**
* Abstract {@link Response} specialization indicating a successful outcome.
*
* <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor
* so subclasses can supply the response body content.
*/
public abstract class SuccessResponse extends Response {
/**
* Create a successful response with the provided body.
*
* @param sessionId the session id this response targets
* @param requestId the originating request id
* @param body the response body
*/
protected SuccessResponse(SessionId sessionId, int requestId, ResponseBody body) {
super(sessionId, requestId, body);
}
/**
* {@inheritDoc}
*
* <p>This implementation returns the fixed {@code +OK} prefix.
*
* @return the {@code +OK} prefix
*/
@Override
public final String prefix() {
return "+OK";
}
}