Skip to content
Closed
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
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@
package org.springframework.ai.content;

import java.io.IOException;
import java.net.URI;
import java.net.URL;

import org.springframework.core.io.Resource;
Expand Down Expand Up @@ -64,6 +65,7 @@
*
* @author Christian Tzolov
* @author Mark Pollack
* @author Thomas Vitale
* @since 1.0.0
*/
public class Media {
Expand All @@ -75,7 +77,7 @@ public class Media {
* media it has been passed.
*/
@Nullable
private String id;
private final String id;

private final MimeType mimeType;

Expand All @@ -98,13 +100,29 @@ public class Media {
* <li>Square brackets
* </ul>
*/
private String name;
private final String name;

/**
* Create a new Media instance.
* @param mimeType the media MIME type
* @param uri the URI for the media data
*/
public Media(MimeType mimeType, URI uri) {
Assert.notNull(mimeType, "MimeType must not be null");
Assert.notNull(uri, "URI must not be null");
this.mimeType = mimeType;
this.id = null;
this.data = uri.toString();
this.name = generateDefaultName(mimeType);
}

/**
* Create a new Media instance.
* @param mimeType the media MIME type
* @param url the URL for the media data
* @deprecated in favour of {@link #Media(MimeType, URI)}
*/
@Deprecated
public Media(MimeType mimeType, URL url) {
Assert.notNull(mimeType, "MimeType must not be null");
Assert.notNull(url, "URL must not be null");
Expand Down Expand Up @@ -138,7 +156,7 @@ public Media(MimeType mimeType, Resource resource) {
* Creates a new Media builder.
* @return a new Media builder instance
*/
public static final Builder builder() {
public static Builder builder() {
return new Builder();
}

Expand All @@ -148,7 +166,7 @@ public static final Builder builder() {
* @param data the media data
* @param id the media id
*/
private Media(MimeType mimeType, Object data, String id, String name) {
private Media(MimeType mimeType, Object data, @Nullable String id, @Nullable String name) {
Assert.notNull(mimeType, "MimeType must not be null");
Assert.notNull(data, "Data must not be null");
this.mimeType = mimeType;
Expand All @@ -171,7 +189,7 @@ public MimeType getMimeType() {

/**
* Get the media data object
* @return a java.net.URL.toString() or a byte[]
* @return a java.net.URI.toString() or a byte[]
*/
public Object getData() {
return this.data;
Expand All @@ -194,6 +212,7 @@ public byte[] getDataAsByteArray() {
* Get the media id
* @return the media id
*/
@Nullable
public String getId() {
return this.id;
}
Expand Down Expand Up @@ -260,12 +279,26 @@ public Builder data(Object data) {
return this;
}

/**
* Sets the media data from a URI.
* @param uri the media URI, must not be null
* @return the builder instance
* @throws IllegalArgumentException if URI is null
*/
public Builder data(URI uri) {
Assert.notNull(uri, "URI must not be null");
this.data = uri.toString();
return this;
}

/**
* Sets the media data from a URL.
* @param url the media URL, must not be null
* @return the builder instance
* @throws IllegalArgumentException if url is null
* @deprecated in favour of {@link #data(URI)}
*/
@Deprecated
public Builder data(URL url) {
Assert.notNull(url, "URL must not be null");
this.data = url.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Core observation abstractions.
*/
package org.springframework.ai.content;
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ or the image URL equivalent:
----
var userMessage = new UserMessage("Explain what do you see on this picture?",
new Media(MimeTypeUtils.IMAGE_PNG,
"https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png"));
URI.create("https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png")));

ChatResponse response = chatModel.call(new Prompt(this.userMessage,
ChatOptions.builder().model(MistralAiApi.ChatModel.PIXTRAL_LARGE.getValue()).build()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ or the image URL equivalent using the `gpt-4o` model:
----
var userMessage = new UserMessage("Explain what do you see on this picture?",
new Media(MimeTypeUtils.IMAGE_PNG,
"https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png"));
URI.create("https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png")));

ChatResponse response = chatModel.call(new Prompt(this.userMessage,
OpenAiChatOptions.builder().model(OpenAiApi.ChatModel.GPT_4_O.getValue()).build()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,6 +18,7 @@

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.UUID;

Expand Down Expand Up @@ -49,6 +50,22 @@ void testMediaBuilderWithByteArrayResource() {
assertThat(media.getName()).isEqualTo(name);
}

@Test
void testMediaBuilderWithUri() {
MimeType mimeType = MimeType.valueOf("image/png");
URI uri = URI.create("http://example.com/image.png");
String id = "123";
String name = "test-media";

Media media = Media.builder().mimeType(mimeType).data(uri).id(id).name(name).build();

assertThat(media.getMimeType()).isEqualTo(mimeType);
assertThat(media.getData()).isInstanceOf(String.class);
assertThat(media.getData()).isEqualTo(uri.toString());
assertThat(media.getId()).isEqualTo(id);
assertThat(media.getName()).isEqualTo(name);
}

@Test
void testMediaBuilderWithURL() throws MalformedURLException {
MimeType mimeType = MimeType.valueOf("image/png");
Expand Down Expand Up @@ -91,6 +108,13 @@ void testGetDataAsByteArrayWithInvalidData() {
.hasMessageContaining("Media data is not a byte[]");
}

@Test
void testMediaBuilderWithNullUri() {
assertThatThrownBy(() -> Media.builder().mimeType(MimeType.valueOf("image/png")).data((URI) null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("URI must not be null");
}

@Test
void testMediaBuilderWithNullURL() {
assertThatThrownBy(() -> Media.builder().mimeType(MimeType.valueOf("image/png")).data((URL) null).build())
Expand Down Expand Up @@ -162,6 +186,20 @@ void testLastDataMethodWins() throws MalformedURLException {
assertThat(media.getData()).isSameAs(bytes);
}

@Test
void testMediaConstructorWithUri() {
MimeType mimeType = MimeType.valueOf("image/png");
URI uri = URI.create("http://example.com/image.png");

Media media = new Media(mimeType, uri);

assertThat(media.getMimeType()).isEqualTo(mimeType);
assertThat(media.getData()).isInstanceOf(String.class);
assertThat(media.getData()).isEqualTo(uri.toString());
assertThat(media.getId()).isNull();
assertValidMediaName(media.getName(), "png");
}

@Test
void testMediaConstructorWithUrl() throws MalformedURLException {
MimeType mimeType = MimeType.valueOf("image/png");
Expand Down Expand Up @@ -195,7 +233,7 @@ private void assertValidMediaName(String name, String expectedMimeSubtype) {
}

@Test
void testMediaConstructorWithResource() throws IOException {
void testMediaConstructorWithResource() {
MimeType mimeType = MimeType.valueOf("image/png");
byte[] data = new byte[] { 1, 2, 3 };
Resource resource = new ByteArrayResource(data);
Expand All @@ -210,7 +248,7 @@ void testMediaConstructorWithResource() throws IOException {
}

@Test
void testMediaConstructorWithResourceAndId() throws IOException {
void testMediaConstructorWithResourceAndId() {
MimeType mimeType = MimeType.valueOf("image/png");
byte[] data = new byte[] { 1, 2, 3 };
Resource resource = new ByteArrayResource(data);
Expand Down Expand Up @@ -256,6 +294,30 @@ public byte[] getContentAsByteArray() throws IOException {

/// Tests to ensure two arg ctors behave identically to the builder

@Test
void testUriConstructorMatchesBuilder() {
// Given
MimeType mimeType = MimeType.valueOf("image/png");
URI uri = URI.create("http://example.com/image.png");

// When
Media mediaFromCtor = new Media(mimeType, uri);
Media mediaFromBuilder = Media.builder().mimeType(mimeType).data(uri).build();

// Then - verify all properties match
assertThat(mediaFromCtor.getMimeType()).isEqualTo(mediaFromBuilder.getMimeType());
assertThat(mediaFromCtor.getData()).isEqualTo(mediaFromBuilder.getData());
assertThat(mediaFromCtor.getId()).isEqualTo(mediaFromBuilder.getId());

// Verify name structure for both instances
assertValidMediaName(mediaFromCtor.getName(), "png");
assertValidMediaName(mediaFromBuilder.getName(), "png");

// Data type consistency
assertThat(mediaFromCtor.getData()).isInstanceOf(String.class);
assertThat(mediaFromBuilder.getData()).isInstanceOf(String.class);
}

@Test
void testURLConstructorMatchesBuilder() throws MalformedURLException {
// Given
Expand Down Expand Up @@ -305,6 +367,30 @@ void testResourceConstructorMatchesBuilder() throws IOException {
assertThat(mediaFromBuilder.getData()).isInstanceOf(byte[].class);
}

@Test
void testUriConstructorNullValidation() {
MimeType mimeType = MimeType.valueOf("image/png");

// Test null mimeType
assertThatThrownBy(() -> new Media(null, URI.create("http://example.com/image.png")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("MimeType must not be null");

// Test null URL
assertThatThrownBy(() -> new Media(mimeType, (URI) null)).isInstanceOf(IllegalArgumentException.class)
.hasMessage("URI must not be null");

// Compare with builder validation
assertThatThrownBy(
() -> Media.builder().mimeType(null).data(URI.create("http://example.com/image.png")).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("MimeType must not be null");

assertThatThrownBy(() -> Media.builder().mimeType(mimeType).data((URI) null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("URI must not be null");
}

@Test
void testURLConstructorNullValidation() {
MimeType mimeType = MimeType.valueOf("image/png");
Expand Down Expand Up @@ -375,7 +461,7 @@ public byte[] getContentAsByteArray() throws IOException {
}

@Test
void testDifferentMimeTypesNameFormat() throws IOException {
void testDifferentMimeTypesNameFormat() {
// Test constructor name generation
Media jpegMediaCtor = new Media(Media.Format.IMAGE_JPEG, new ByteArrayResource(new byte[] { 1, 2, 3 }));
assertValidMediaName(jpegMediaCtor.getName(), "jpeg");
Expand Down