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
@@ -1,6 +1,7 @@
package org.hiero.base;

import com.hedera.hashgraph.sdk.FileId;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Objects;
import org.jspecify.annotations.NonNull;
Expand Down Expand Up @@ -92,6 +93,55 @@ default void deleteFile(@NonNull String fileId) throws HieroException {
void updateExpirationTime(@NonNull FileId fileId, @NonNull Instant expirationTime)
throws HieroException;

/**
* Appends new file content to the end of an existing file.
*
* @param fileId the ID of the file to append
* @param content the content to append
* @throws HieroException if the content could not be appended to the file
*/
default void appendFile(@NonNull String fileId, @NonNull String content) throws HieroException {
Objects.requireNonNull(fileId, "fileId must not be null");
Objects.requireNonNull(content, "content must not be null");

appendFile(FileId.fromString(fileId), content.getBytes(StandardCharsets.UTF_8));
}

/**
* Appends new file content to the end of an existing file.
*
* @param fileId the ID of the file to append
* @param content the content to append
* @throws HieroException if the content could not be appended to the file
*/
default void appendFile(@NonNull FileId fileId, @NonNull String content) throws HieroException {
Objects.requireNonNull(fileId, "fileId must not be null");
Objects.requireNonNull(content, "content must not be null");

appendFile(fileId, content.getBytes(StandardCharsets.UTF_8));
}

/**
* Appends new file content to the end of an existing file.
*
* @param fileId the ID of the file to append
* @param content the byte content to append
* @throws HieroException if the content could not be appended to the file
*/
default void appendFile(@NonNull String fileId, byte[] content) throws HieroException {
Objects.requireNonNull(fileId, "fileId must not be null");
appendFile(FileId.fromString(fileId), content);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Appends new file content to the end of an existing file.
*
* @param fileId the ID of the file to append
* @param content the byte content to append
* @throws HieroException if the content could not be appended to the file
*/
void appendFile(@NonNull FileId fileId, byte[] content) throws HieroException;

/**
* Check if a file is deleted.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,7 @@ private FileId createFileImpl(
final FileId fileId = result.fileId();
byte[] remaining =
Arrays.copyOfRange(contents, FileCreateRequest.FILE_CREATE_MAX_SIZE, contents.length);
while (remaining.length > 0) {
final int length = Math.min(remaining.length, FileCreateRequest.FILE_CREATE_MAX_SIZE);
final byte[] next = Arrays.copyOf(remaining, length);
final FileAppendRequest appendRequest = FileAppendRequest.of(fileId, next);
protocolLayerClient.executeFileAppendRequestTransaction(appendRequest);
remaining = Arrays.copyOfRange(remaining, length, remaining.length);
}
appendRemainingChunks(fileId, remaining);
return fileId;
}
}
Expand Down Expand Up @@ -139,13 +133,7 @@ public void updateFile(@NonNull final FileId fileId, @NonNull final byte[] conte
protocolLayerClient.executeFileUpdateRequestTransaction(request);
byte[] remaining =
Arrays.copyOfRange(content, FileCreateRequest.FILE_CREATE_MAX_SIZE, content.length);
while (remaining.length > 0) {
final int length = Math.min(remaining.length, FileCreateRequest.FILE_CREATE_MAX_SIZE);
byte[] next = Arrays.copyOf(remaining, length);
final FileAppendRequest appendRequest = FileAppendRequest.of(fileId, next);
protocolLayerClient.executeFileAppendRequestTransaction(appendRequest);
remaining = Arrays.copyOfRange(remaining, length, remaining.length);
}
appendRemainingChunks(fileId, remaining);
}
}

Expand All @@ -162,6 +150,43 @@ public void updateExpirationTime(
protocolLayerClient.executeFileUpdateRequestTransaction(request);
}

@Override
public void appendFile(@NonNull FileId fileId, byte[] content) throws HieroException {
Objects.requireNonNull(fileId, "fileId must not be null");
Objects.requireNonNull(content, "content must not be null");

if (content.length > FileCreateRequest.FILE_MAX_SIZE) {
throw new HieroException(
"File contents must be less than " + FileCreateRequest.FILE_MAX_SIZE + " bytes");
}

int initialSize = getSize(fileId);
if (initialSize + content.length > FileCreateRequest.FILE_MAX_SIZE) {
throw new HieroException(
"File contents must be less than " + FileCreateRequest.FILE_MAX_SIZE + " bytes");
}

if (content.length <= FileCreateRequest.FILE_CREATE_MAX_SIZE) {
final FileAppendRequest request = FileAppendRequest.of(fileId, content);
protocolLayerClient.executeFileAppendRequestTransaction(request);
} else {
if (log.isDebugEnabled()) {
int appendCount = Math.ceilDiv(content.length, FileCreateRequest.FILE_CREATE_MAX_SIZE);
log.debug(
"Content of size {} is to big for 1 FileAppend transaction. Will append {} FileAppend transactions",
content.length,
appendCount);
}

byte[] start = Arrays.copyOf(content, FileCreateRequest.FILE_CREATE_MAX_SIZE);
final FileAppendRequest request = FileAppendRequest.of(fileId, start);
protocolLayerClient.executeFileAppendRequestTransaction(request);
byte[] remaining =
Arrays.copyOfRange(content, FileCreateRequest.FILE_CREATE_MAX_SIZE, content.length);
appendRemainingChunks(fileId, remaining);
}
}

@Override
public boolean isDeleted(@NonNull final FileId fileId) throws HieroException {
Objects.requireNonNull(fileId, "fileId must not be null");
Expand All @@ -185,4 +210,15 @@ public Instant getExpirationTime(@NonNull final FileId fileId) throws HieroExcep
final FileInfoResponse infoResponse = protocolLayerClient.executeFileInfoQuery(request);
return infoResponse.expirationTime();
}

private void appendRemainingChunks(@NonNull FileId fileId, byte[] remaining)
throws HieroException {
while (remaining.length > 0) {
final int length = Math.min(remaining.length, FileCreateRequest.FILE_CREATE_MAX_SIZE);
final byte[] next = Arrays.copyOf(remaining, length);
final FileAppendRequest appendRequest = FileAppendRequest.of(fileId, next);
protocolLayerClient.executeFileAppendRequestTransaction(appendRequest);
remaining = Arrays.copyOfRange(remaining, length, remaining.length);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public record FileAppendRequest(
throw new IllegalArgumentException(
"File contents must be less than " + FILE_CREATE_MAX_BYTES + " bytes");
}

if (fileMemo != null && fileMemo.length() > 100) {
throw new IllegalArgumentException("File memo must be less than 100 characters");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import com.hedera.hashgraph.sdk.FileId;
import java.time.Instant;
import java.util.List;
import org.hiero.base.HieroException;
import org.hiero.base.implementation.FileClientImpl;
import org.hiero.base.protocol.ProtocolLayerClient;
Expand All @@ -28,12 +31,16 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

public class FileClientImplTest {
ProtocolLayerClient protocolLayerClient;
FileClientImpl fileClientImpl;

ArgumentCaptor<FileAppendRequest> fileAppendCaptor =
ArgumentCaptor.forClass(FileAppendRequest.class);

@BeforeEach
void setup() {
protocolLayerClient = Mockito.mock(ProtocolLayerClient.class);
Expand Down Expand Up @@ -229,6 +236,136 @@ void testUpdateFileThrowsExceptionForNullArguments() {
NullPointerException.class, () -> fileClientImpl.updateFile(null, null));
}

@Test
void testAppendFile() throws HieroException {
// mocks
final FileAppendResult result = Mockito.mock(FileAppendResult.class);
final FileInfoResponse fileInfoResponse = Mockito.mock(FileInfoResponse.class);

// given
final FileId fileId = FileId.fromString("1.2.3");
final byte[] content = "Hello, Hiero!".getBytes();

// then
when(protocolLayerClient.executeFileAppendRequestTransaction(any(FileAppendRequest.class)))
.thenReturn(result);
when(protocolLayerClient.executeFileInfoQuery(any(FileInfoRequest.class)))
.thenReturn(fileInfoResponse);
when(fileInfoResponse.size()).thenReturn(0);

fileClientImpl.appendFile(fileId, content);

verify(protocolLayerClient, times(1))
.executeFileAppendRequestTransaction(fileAppendCaptor.capture());
verify(protocolLayerClient, times(1)).executeFileInfoQuery(any(FileInfoRequest.class));

final FileAppendRequest captureRequest = fileAppendCaptor.getValue();
Assertions.assertEquals(fileId, captureRequest.fileId());
Assertions.assertArrayEquals(content, captureRequest.contents());
}

@Test
void testAppendFileThrowsExceptionWhenContentExceedsMaxSize() throws HieroException {
// given
final FileId fileId = FileId.fromString("1.2.3");
final byte[] content = new byte[FileCreateRequest.FILE_MAX_SIZE + 1];

final HieroException err =
Assertions.assertThrows(
HieroException.class, () -> fileClientImpl.appendFile(fileId, content));

Assertions.assertEquals(
"File contents must be less than " + FileCreateRequest.FILE_MAX_SIZE + " bytes",
err.getMessage());

verifyNoInteractions(protocolLayerClient);
}

@Test
void testAppendFileThrowsExceptionWhenCombinedSizeExceedsMaxSize() throws HieroException {
// mocks
final FileInfoResponse fileInfoResponse = Mockito.mock(FileInfoResponse.class);

// given
final FileId fileId = FileId.fromString("1.2.3");
final byte[] content = new byte[10];

when(protocolLayerClient.executeFileInfoQuery(any(FileInfoRequest.class)))
.thenReturn(fileInfoResponse);
when(fileInfoResponse.size()).thenReturn(FileCreateRequest.FILE_MAX_SIZE);

final HieroException err =
Assertions.assertThrows(
HieroException.class, () -> fileClientImpl.appendFile(fileId, content));

Assertions.assertEquals(
"File contents must be less than " + FileCreateRequest.FILE_MAX_SIZE + " bytes",
err.getMessage());

verify(protocolLayerClient, times(1)).executeFileInfoQuery(any(FileInfoRequest.class));
verify(protocolLayerClient, never())
.executeFileAppendRequestTransaction(any(FileAppendRequest.class));
}

@Test
void testAppendFileWithLargeContent() throws HieroException {
// mocks
final FileInfoResponse fileInfoResponse = Mockito.mock(FileInfoResponse.class);

// given
final FileId fileId = FileId.fromString("1.2.3");
final byte[] content = new byte[FileCreateRequest.FILE_CREATE_MAX_SIZE * 2];
Comment thread
manishdait marked this conversation as resolved.

when(protocolLayerClient.executeFileInfoQuery(any(FileInfoRequest.class)))
.thenReturn(fileInfoResponse);
when(fileInfoResponse.size()).thenReturn(1);

fileClientImpl.appendFile(fileId, content);

// call for each 2048 chunk
verify(protocolLayerClient, times(2))
.executeFileAppendRequestTransaction(fileAppendCaptor.capture());
verify(protocolLayerClient, times(1)).executeFileInfoQuery(any(FileInfoRequest.class));

final List<FileAppendRequest> capturedRequests = fileAppendCaptor.getAllValues();
Assertions.assertEquals(fileId, capturedRequests.get(0).fileId());
Assertions.assertEquals(
FileCreateRequest.FILE_CREATE_MAX_SIZE, capturedRequests.get(0).contents().length);

Assertions.assertEquals(fileId, capturedRequests.get(1).fileId());
Assertions.assertEquals(
FileCreateRequest.FILE_CREATE_MAX_SIZE, capturedRequests.get(1).contents().length);
}

@Test
void testAppendFileWithLargeContentUnevenSize() throws HieroException {
// mocks
final FileInfoResponse fileInfoResponse = Mockito.mock(FileInfoResponse.class);

// given
final FileId fileId = FileId.fromString("1.2.3");
final byte[] content = new byte[FileCreateRequest.FILE_CREATE_MAX_SIZE + 2];

when(protocolLayerClient.executeFileInfoQuery(any(FileInfoRequest.class)))
.thenReturn(fileInfoResponse);
when(fileInfoResponse.size()).thenReturn(1);

fileClientImpl.appendFile(fileId, content);

// call for each chunk 2048 and 2
verify(protocolLayerClient, times(2))
.executeFileAppendRequestTransaction(fileAppendCaptor.capture());
verify(protocolLayerClient, times(1)).executeFileInfoQuery(any(FileInfoRequest.class));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

final List<FileAppendRequest> capturedRequests = fileAppendCaptor.getAllValues();
Assertions.assertEquals(fileId, capturedRequests.get(0).fileId());
Assertions.assertEquals(
FileCreateRequest.FILE_CREATE_MAX_SIZE, capturedRequests.get(0).contents().length);

Assertions.assertEquals(fileId, capturedRequests.get(1).fileId());
Assertions.assertEquals(2, capturedRequests.get(1).contents().length);
}

Comment thread
manishdait marked this conversation as resolved.
@Test
void testGetFileSize() throws HieroException {
// mocks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.hiero.base.FileClient;
import org.hiero.base.HieroException;
import org.hiero.base.protocol.data.FileCreateRequest;
import org.hiero.microprofile.ClientProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -40,4 +42,41 @@ void testFileClient() throws Exception {
// then
Assertions.assertNotNull(fileId);
}

@Test
void testAppendFile() throws HieroException {
final byte[] contents = "Hello,".getBytes();
final FileId fileId = fileClient.createFile(contents);

Assertions.assertNotNull(fileId);

fileClient.appendFile(fileId, " Hiero!");
byte[] bytes = fileClient.readFile(fileId);

Assertions.assertNotNull(bytes);
Assertions.assertEquals("Hello, Hiero!", new String(bytes));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Test
void testAppendFileThrowsExceptionWhenContentExceedMaxSize() throws HieroException {
final byte[] content = new byte[FileCreateRequest.FILE_MAX_SIZE + 1];
final FileId fileId = fileClient.createFile(new byte[0]);

Assertions.assertNotNull(fileId);
Assertions.assertThrows(HieroException.class, () -> fileClient.appendFile(fileId, content));
}

@Test
void testChunkedAppendFile() throws HieroException {
final String content = "A".repeat(FileCreateRequest.FILE_CREATE_MAX_SIZE * 2);
final FileId fileId = fileClient.createFile(new byte[0]);

Assertions.assertNotNull(fileId);

fileClient.appendFile(fileId, content);
byte[] bytes = fileClient.readFile(fileId);

Assertions.assertNotNull(bytes);
Assertions.assertEquals(content, new String(bytes));
}
}
Loading