Skip to content

Commit f1fc2f8

Browse files
committed
3814 Handle null mimeType and filename gracefully in FileEntry and related components.
1 parent 82b7fb0 commit f1fc2f8

File tree

10 files changed

+187
-32
lines changed

10 files changed

+187
-32
lines changed

server/ee/libs/embedded/embedded-execution/embedded-execution-public-rest/src/main/java/com/bytechef/ee/embedded/execution/public_/web/rest/ActionApiController.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Objects;
2626
import org.springframework.http.ResponseEntity;
27+
import org.springframework.lang.Nullable;
2728
import org.springframework.web.bind.WebDataBinder;
2829
import org.springframework.web.bind.annotation.InitBinder;
2930
import org.springframework.web.bind.annotation.RequestMapping;
@@ -85,9 +86,7 @@ public ResponseEntity<Object> executeAction(
8586
String filename = MapUtils.getRequiredString(entryValue, FILENAME);
8687
String value = MapUtils.getRequiredString(entryValue, VALUE);
8788

88-
bodyContent.put(
89-
entry.getKey(),
90-
new FileEntryImpl(new FileEntry(filename, URL_PREFIX + value)));
89+
bodyContent.put(entry.getKey(), new FileEntryImpl(new FileEntry(filename, URL_PREFIX + value)));
9190
} else {
9291
bodyContent.put(entry.getKey(), entryValue.get(VALUE));
9392
}
@@ -110,7 +109,9 @@ public void initBinder(WebDataBinder dataBinder) {
110109

111110
private static class FileEntryImpl implements com.bytechef.component.definition.FileEntry {
112111

112+
@Nullable
113113
private String extension;
114+
@Nullable
114115
private String mimeType;
115116
private String name;
116117
private String url;
@@ -119,23 +120,24 @@ private FileEntryImpl() {
119120
}
120121

121122
public FileEntryImpl(FileEntry fileEntry) {
122-
this(fileEntry.getExtension(), fileEntry.getMimeType(), fileEntry.getName(), fileEntry.getUrl());
123+
this(fileEntry.getName(), fileEntry.getExtension(), fileEntry.getMimeType(), fileEntry.getUrl());
123124
}
124125

125-
public FileEntryImpl(String extension, String mimeType, String name, String url) {
126-
this.extension = Objects.requireNonNull(extension);
127-
this.mimeType = Objects.requireNonNull(mimeType);
126+
public FileEntryImpl(String name, @Nullable String extension, @Nullable String mimeType, String url) {
127+
this.extension = extension;
128+
this.mimeType = mimeType;
128129
this.name = Objects.requireNonNull(name);
129130
this.url = Objects.requireNonNull(url);
130131
}
131132

132133
@Override
133-
134+
@Nullable
134135
public String getExtension() {
135136
return extension;
136137
}
137138

138139
@Override
140+
@Nullable
139141
public String getMimeType() {
140142
return mimeType;
141143
}

server/libs/core/file-storage/file-storage-api/src/main/java/com/bytechef/file/storage/domain/FileEntry.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.nio.charset.StandardCharsets;
2323
import java.util.Map;
2424
import java.util.Objects;
25+
import org.springframework.lang.Nullable;
2526
import org.springframework.util.Assert;
2627

2728
/**
@@ -62,7 +63,7 @@ public FileEntry(String filename, String url) {
6263
this.url = url;
6364
}
6465

65-
public FileEntry(String name, String extension, String mimeType, String url) {
66+
public FileEntry(String name, @Nullable String extension, @Nullable String mimeType, String url) {
6667
Assert.notNull(name, "'name' must not be null");
6768
Assert.notNull(url, "'url' must not be null");
6869

@@ -112,11 +113,11 @@ public int hashCode() {
112113
return Objects.hash(name, url);
113114
}
114115

115-
public String getExtension() {
116+
public @Nullable String getExtension() {
116117
return extension;
117118
}
118119

119-
public String getMimeType() {
120+
public @Nullable String getMimeType() {
120121
return mimeType;
121122
}
122123

@@ -138,9 +139,9 @@ public String toId() {
138139
@Override
139140
public String toString() {
140141
return "FileEntry{" +
141-
"extension='" + extension + '\'' +
142+
"name='" + name + '\'' +
143+
", extension='" + extension + '\'' +
142144
", mimeType='" + mimeType + '\'' +
143-
", name='" + name + '\'' +
144145
", url='" + url + '\'' +
145146
'}';
146147
}

server/libs/modules/components/google/google-mail/src/main/java/com/bytechef/component/google/mail/util/GoogleMailUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ private static List<FileEntry> getFileEntries(Message message, Context context,
458458
fileEntries.add(
459459
context.file(
460460
file -> file.storeContent(
461-
messagePart.getFilename(), new ByteArrayInputStream(attachment.decodeData()))));
461+
messagePart.getFilename() == null ? "attachment" : messagePart.getFilename(),
462+
new ByteArrayInputStream(attachment.decodeData()))));
462463
}
463464
}
464465

server/libs/modules/components/google/google-mail/src/test/java/com/bytechef/component/google/mail/util/GoogleMailUtilsTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,25 @@
3535
import static org.junit.jupiter.api.Assertions.assertEquals;
3636
import static org.junit.jupiter.api.Assertions.assertNotNull;
3737
import static org.junit.jupiter.api.Assertions.assertTrue;
38+
import static org.mockito.ArgumentMatchers.any;
3839
import static org.mockito.ArgumentMatchers.anyString;
3940
import static org.mockito.Mockito.mock;
4041
import static org.mockito.Mockito.mockStatic;
42+
import static org.mockito.Mockito.times;
43+
import static org.mockito.Mockito.verify;
4144
import static org.mockito.Mockito.when;
4245

4346
import com.bytechef.component.definition.ActionContext;
4447
import com.bytechef.component.definition.ComponentDsl.ModifiableObjectProperty;
48+
import com.bytechef.component.definition.Context;
49+
import com.bytechef.component.definition.FileEntry;
4550
import com.bytechef.component.definition.Option;
4651
import com.bytechef.component.definition.Parameters;
4752
import com.bytechef.component.google.mail.definition.Format;
4853
import com.bytechef.component.test.definition.MockParametersFactory;
4954
import com.bytechef.google.commons.GoogleServices;
5055
import com.google.api.services.gmail.Gmail;
56+
import com.google.api.services.gmail.Gmail.Users.Messages.Attachments;
5157
import com.google.api.services.gmail.model.Label;
5258
import com.google.api.services.gmail.model.ListLabelsResponse;
5359
import com.google.api.services.gmail.model.Message;
@@ -60,6 +66,7 @@
6066
import jakarta.mail.internet.MimeMultipart;
6167
import java.io.ByteArrayInputStream;
6268
import java.io.IOException;
69+
import java.io.InputStream;
6370
import java.math.BigInteger;
6471
import java.util.List;
6572
import java.util.Map;
@@ -174,6 +181,78 @@ void testGetSimpleMessage() {
174181
assertEquals(expectedSimpleMessage, result);
175182
}
176183

184+
@Test
185+
@SuppressWarnings("unchecked")
186+
void testGetSimpleMessageWithAttachments() throws Exception {
187+
Message message = new Message()
188+
.setId("id")
189+
.setThreadId("threadId")
190+
.setHistoryId(new BigInteger("123"))
191+
.setPayload(
192+
new MessagePart()
193+
.setMimeType("multipart/mixed")
194+
.setHeaders(List.of(
195+
new MessagePartHeader()
196+
.setName("Subject")
197+
.setValue("email subject"),
198+
new MessagePartHeader()
199+
.setName("Content-Type")
200+
.setValue("multipart/mixed")))
201+
.setParts(List.of(
202+
new MessagePart()
203+
.setMimeType("text/plain")
204+
.setBody(new MessagePartBody().setData("ZW1haWwgYm9keQ==")),
205+
new MessagePart()
206+
.setMimeType("application/pdf")
207+
.setFilename(null)
208+
.setBody(new MessagePartBody().setAttachmentId("attachmentId1")),
209+
new MessagePart()
210+
.setMimeType("application/octet-stream")
211+
.setFilename("noextension")
212+
.setBody(new MessagePartBody().setAttachmentId("attachmentId2")))));
213+
214+
Attachments mockedAttachments = mock(Attachments.class);
215+
Attachments.Get mockedAttachmentsGet = mock(Attachments.Get.class);
216+
217+
when(mockedGmail.users()).thenReturn(mockedUsers);
218+
when(mockedUsers.messages()).thenReturn(mockedMessages);
219+
when(mockedMessages.attachments()).thenReturn(mockedAttachments);
220+
when(mockedAttachments.get(anyString(), anyString(), anyString())).thenReturn(mockedAttachmentsGet);
221+
when(mockedAttachmentsGet.execute()).thenReturn(new MessagePartBody().setData("YXRhY2htZW50IGNvbnRlbnQ="));
222+
223+
Context.File mockedFile = mock(Context.File.class);
224+
225+
when(mockedActionContext.file(any())).thenAnswer(invocation -> {
226+
Context.ContextFunction<Context.File, ?> function = invocation.getArgument(0);
227+
228+
return function.apply(mockedFile);
229+
});
230+
231+
FileEntry mockedFileEntry1 = mock(FileEntry.class);
232+
FileEntry mockedFileEntry2 = mock(FileEntry.class);
233+
234+
when(mockedFile.storeContent(anyString(), any(InputStream.class))).thenReturn(
235+
mockedFileEntry1, mockedFileEntry2);
236+
237+
GoogleMailUtils.SimpleMessage result = GoogleMailUtils.getSimpleMessage(
238+
message, mockedActionContext, mockedGmail);
239+
240+
assertNotNull(result);
241+
242+
List<FileEntry> attachments = result.attachments();
243+
244+
assertEquals(2, attachments.size());
245+
246+
ArgumentCaptor<String> fileNameCaptor = ArgumentCaptor.forClass(String.class);
247+
248+
verify(mockedFile, times(2)).storeContent(fileNameCaptor.capture(), any(InputStream.class));
249+
250+
List<String> capturedFileNames = fileNameCaptor.getAllValues();
251+
252+
assertEquals("attachment", capturedFileNames.get(0));
253+
assertEquals("noextension", capturedFileNames.get(1));
254+
}
255+
177256
@Test
178257
void tesGetMessageOutputForSimpleFormat() {
179258
ModifiableObjectProperty messageOutputProperty = GoogleMailUtils.getMessageOutputProperty(Format.SIMPLE);

server/libs/platform/platform-component/platform-component-context/platform-component-context-service/src/main/java/com/bytechef/platform/component/context/FileEntryImpl.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,34 @@
1919
import com.bytechef.file.storage.domain.FileEntry;
2020
import com.fasterxml.jackson.annotation.JsonIgnore;
2121
import java.util.Objects;
22+
import org.jspecify.annotations.Nullable;
2223

2324
/**
2425
* @author Ivica Cardic
2526
*/
2627
public class FileEntryImpl implements com.bytechef.component.definition.FileEntry {
2728

28-
private String extension;
29-
private String mimeType;
29+
private @Nullable String extension;
30+
private @Nullable String mimeType;
3031
private String name;
3132
private String url;
3233

3334
private FileEntryImpl() {
3435
}
3536

3637
public FileEntryImpl(FileEntry fileEntry) {
37-
this(fileEntry.getExtension(), fileEntry.getMimeType(), fileEntry.getName(), fileEntry.getUrl());
38+
this(fileEntry.getName(), fileEntry.getExtension(), fileEntry.getMimeType(), fileEntry.getUrl());
3839
}
3940

40-
public FileEntryImpl(String extension, String mimeType, String name, String url) {
41-
this.extension = Objects.requireNonNull(extension);
42-
this.mimeType = Objects.requireNonNull(mimeType);
41+
public FileEntryImpl(String name, @Nullable String extension, @Nullable String mimeType, String url) {
42+
this.extension = extension;
43+
this.mimeType = mimeType;
4344
this.name = Objects.requireNonNull(name);
4445
this.url = Objects.requireNonNull(url);
4546
}
4647

4748
@Override
48-
49-
public String getExtension() {
49+
public @Nullable String getExtension() {
5050
return extension;
5151
}
5252

@@ -56,7 +56,7 @@ public FileEntry getFileEntry() {
5656
}
5757

5858
@Override
59-
public String getMimeType() {
59+
public @Nullable String getMimeType() {
6060
return mimeType;
6161
}
6262

@@ -73,9 +73,9 @@ public String getUrl() {
7373
@Override
7474
public String toString() {
7575
return "FileEntryImpl{" +
76-
"extension='" + extension + '\'' +
76+
"name='" + name + '\'' +
77+
", extension='" + extension + '\'' +
7778
", mimeType='" + mimeType + '\'' +
78-
", name='" + name + '\'' +
7979
", url='" + url + '\'' +
8080
'}';
8181
}

server/libs/platform/platform-component/platform-component-context/platform-component-context-service/src/main/java/com/bytechef/platform/component/context/jackson/FileEntryDeserializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public FileEntry deserialize(JsonParser jp, DeserializationContext ctxt) {
3535
JsonNode jsonNode = ctxt.readTree(jp);
3636

3737
return new FileEntryImpl(
38-
asText("extension", jsonNode), asText("mimeType", jsonNode), asText("name", jsonNode),
38+
asText("name", jsonNode), asText("extension", jsonNode), asText("mimeType", jsonNode),
3939
asText("url", jsonNode));
4040
}
4141

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2025 ByteChef
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.bytechef.platform.component.context;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
21+
22+
import com.bytechef.file.storage.domain.FileEntry;
23+
import org.junit.jupiter.api.Test;
24+
25+
/**
26+
* @author Ivica Cardic
27+
*/
28+
public class FileEntryImplTest {
29+
30+
@Test
31+
public void testConstructorWithNulls() {
32+
FileEntryImpl fileEntryImpl = new FileEntryImpl("filename", null, null, "url");
33+
34+
assertNull(fileEntryImpl.getExtension());
35+
assertNull(fileEntryImpl.getMimeType());
36+
assertEquals("filename", fileEntryImpl.getName());
37+
assertEquals("url", fileEntryImpl.getUrl());
38+
}
39+
40+
@Test
41+
public void testConstructorWithFileEntry() {
42+
FileEntry fileEntry = new FileEntry("filename.txt", "url");
43+
44+
FileEntryImpl fileEntryImpl = new FileEntryImpl(fileEntry);
45+
46+
assertEquals("txt", fileEntryImpl.getExtension());
47+
assertEquals("text/plain", fileEntryImpl.getMimeType());
48+
assertEquals("filename.txt", fileEntryImpl.getName());
49+
assertEquals("url", fileEntryImpl.getUrl());
50+
}
51+
52+
@Test
53+
public void testGetFileEntry() {
54+
FileEntryImpl fileEntryImpl = new FileEntryImpl("filename.txt", "txt", "text/plain", "url");
55+
56+
FileEntry fileEntry = fileEntryImpl.getFileEntry();
57+
58+
assertEquals("filename.txt", fileEntry.getName());
59+
assertEquals("url", fileEntry.getUrl());
60+
// Note: FileEntry(name, url) constructor calculates extension/mimeType from name
61+
assertEquals("txt", fileEntry.getExtension());
62+
assertEquals("text/plain", fileEntry.getMimeType());
63+
}
64+
}

server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/jackson/WebhookRequestDeserializer.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Map;
2828
import java.util.Objects;
2929
import org.jspecify.annotations.NonNull;
30+
import org.jspecify.annotations.Nullable;
3031
import org.springframework.boot.jackson.JacksonComponent;
3132
import tools.jackson.core.JsonParser;
3233
import tools.jackson.databind.DeserializationContext;
@@ -142,20 +143,21 @@ private static class FileEntryImpl implements com.bytechef.component.definition.
142143
private FileEntryImpl() {
143144
}
144145

145-
public FileEntryImpl(String name, String extension, String mimeType, String url) {
146-
this.extension = Objects.requireNonNull(extension);
147-
this.mimeType = Objects.requireNonNull(mimeType);
146+
public FileEntryImpl(String name, @Nullable String extension, @Nullable String mimeType, String url) {
147+
this.extension = extension;
148+
this.mimeType = mimeType;
148149
this.name = Objects.requireNonNull(name);
149150
this.url = Objects.requireNonNull(url);
150151
}
151152

152153
@Override
153-
154+
@Nullable
154155
public String getExtension() {
155156
return extension;
156157
}
157158

158159
@Override
160+
@Nullable
159161
public String getMimeType() {
160162
return mimeType;
161163
}

0 commit comments

Comments
 (0)