Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,7 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu
@Nullable ClientCallContext context) throws A2AClientException {
checkNotNullParam("request", request);
checkNotNullParam("taskId", request.taskId());
if(request.id() == null) {
throw new IllegalArgumentException("Id must not be null");
}
checkNotNullParam("id", request.id());

org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest grpcRequest = org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.newBuilder()
.setTaskId(request.taskId())
Expand Down Expand Up @@ -333,7 +331,7 @@ public void subscribeToTask(TaskIdParams request, Consumer<StreamingEventKind> e
A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders);
stubWithMetadata.subscribeToTask(grpcRequest, streamObserver);
} catch (StatusRuntimeException e) {
throw GrpcErrorMapper.mapGrpcError(e, "Failed to subscribe task push notification config: ");
throw GrpcErrorMapper.mapGrpcError(e, "Failed to subscribe to task: ");
}
}

Expand Down
39 changes: 31 additions & 8 deletions spec/src/main/java/org/a2aproject/sdk/spec/DataPart.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import com.google.gson.ToNumberPolicy;
import org.a2aproject.sdk.util.Assert;
import org.a2aproject.sdk.spec.util.CollectionCopies;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;

Expand All @@ -23,7 +26,6 @@
* <li>JSON objects: {@code Map<String, Object>}</li>
* <li>JSON arrays: {@code List<Object>}</li>
* <li>Primitives: {@code String}, {@code Number}, {@code Boolean}</li>
* <li>Null values: {@code null}</li>
* </ul>
* <p>
* Example usage:
Expand All @@ -42,7 +44,7 @@
* DataPart primitive = new DataPart(42);
* }</pre>
*
* @param data the structured data (required, supports JSON objects, arrays, primitives, and null)
* @param data the structured data (required, supports JSON objects, arrays, and primitives)
* @param metadata additional metadata for the part
* @see Part
* @see Message
Expand All @@ -59,24 +61,25 @@ public record DataPart(Object data, @Nullable Map<String, Object> metadata) impl
public static final String DATA = "data";

/**
* Compact constructor with validation.
* Compact constructor with validation and defensive copying.
* <p>
* Note: For mutable data types (Map, List), callers should ensure immutability
* by using {@code Map.copyOf()} or {@code List.copyOf()} before passing to this constructor.
* For mutable data types ({@code Map} and {@code List}), an unmodifiable defensive
* copy is created. Primitives and other immutable values are stored as-is.
*
* @param data the structured data (supports JSON objects, arrays, primitives, and null)
* @param data the structured data (required, supports JSON objects, arrays, and primitives)
* @param metadata additional metadata for the part
* @throws IllegalArgumentException if data is null
*/
public DataPart (Object data, @Nullable Map<String, Object> metadata) {
Assert.checkNotNullParam("data", data);
this.metadata = CollectionCopies.unmodifiableNullableShallowMap(metadata);
this.data = data;
this.data = defensivelyCopy(data);
}

/**
* Constructor.
*
* @param data the structured data (supports JSON objects, arrays, primitives, and not null)
* @param data the structured data (required, supports JSON objects, arrays, and primitives)
* @throws IllegalArgumentException if data is null
*/
public DataPart(Object data) {
Expand Down Expand Up @@ -137,4 +140,24 @@ public static DataPart fromJson(String json, @Nullable Map<String, Object> metad
private static final Gson JSON_PARSER = new GsonBuilder()
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.create();

/**
* Creates a defensive copy of mutable collection types to ensure immutability.
* <p>
* For {@code Map} and {@code List} instances, returns an unmodifiable copy preserving
* null elements. For all other types (primitives, Strings, immutable objects), returns
* the value as-is.
*
* @param data the data value to potentially copy
* @return an unmodifiable copy for collections, or the original value for immutable types
*/
private static Object defensivelyCopy(Object data) {
if (data instanceof Map<?, ?> map) {
return CollectionCopies.unmodifiableShallowMap(map);
}
if (data instanceof List<?> list) {
return Collections.unmodifiableList(new ArrayList<>(list));
}
return data;
}
}
60 changes: 60 additions & 0 deletions spec/src/test/java/org/a2aproject/sdk/spec/DataPartTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -108,4 +110,62 @@ void testFromJson_nullLiteralThrows() {
void testFromJson_invalidJsonThrows() {
assertThrows(IllegalArgumentException.class, () -> DataPart.fromJson("{invalid}"));
}

@SuppressWarnings("unchecked")
@Test
void testDataMapIsDefensivelyCopiedAndImmutable() {
Map<String, Object> data = new HashMap<>();
data.put("temperature", 22.5);

DataPart part = new DataPart(data);

assertThrows(UnsupportedOperationException.class, () -> ((Map<String, Object>) part.data()).put("humidity", 65));
data.put("humidity", 65);
assertEquals(Map.of("temperature", 22.5), part.data());
}

@Test
void testDataListIsDefensivelyCopiedAndImmutable() {
List<Object> data = new ArrayList<>();
data.add("a");

DataPart part = new DataPart(data);

assertThrows(UnsupportedOperationException.class, () -> ((List<Object>) part.data()).add("b"));
data.add("b");
assertEquals(List.of("a"), part.data());
}

@Test
void testDataMapPreservesNullValues() {
Map<String, Object> data = new HashMap<>();
data.put("source", null);

DataPart part = new DataPart(data);

assertTrue(part.data() instanceof Map);
assertTrue(((Map<?, ?>) part.data()).containsKey("source"));
assertNull(((Map<?, ?>) part.data()).get("source"));
}

@Test
void testDataListPreservesNullElements() {
List<Object> data = new ArrayList<>();
data.add(null);

DataPart part = new DataPart(data);

assertTrue(part.data() instanceof List);
assertEquals(1, ((List<?>) part.data()).size());
assertNull(((List<?>) part.data()).get(0));
}

@Test
void testDataPrimitiveStoredAsIs() {
Integer data = 42;

DataPart part = new DataPart(data);

assertSame(data, part.data());
}
}
Loading