2
\$\begingroup\$

I've created a service that connects to Dropbox and makes it possible to download and upload files, create and list folders and remove file or folder.

Full code is available on GitHub: https://github.com/MajewskiKrzysztof/spring-boot-dropbox

DropboxConfiguration

@Configuration
class DropboxConfiguration {

    @Value("${dropbox.accessToken}")
    private String accessToken;

    @Bean
    public DbxClientV2 dropboxClient() {
        DbxRequestConfig config = DbxRequestConfig.newBuilder("example-app").build();
        return new DbxClientV2(config, accessToken);
    }

}

DropboxServiceImpl

@Service
class DropboxServiceImpl implements DropboxService {

    private final DbxClientV2 client;

    public DropboxServiceImpl(DbxClientV2 client) {
        this.client = client;
    }

    @Override
    public InputStream downloadFile(String filePath) throws DropboxException {
        return handleDropboxAction(() -> client.files().download(filePath).getInputStream(),
                String.format("Error downloading file: %s", filePath));
    }

    @Override
    public FileMetadata uploadFile(String filePath, InputStream fileStream) throws DropboxException {
        return handleDropboxAction(() -> client.files().uploadBuilder(filePath).uploadAndFinish(fileStream),
                String.format("Error uploading file: %s", filePath));
    }

    @Override
    public CreateFolderResult createFolder(String folderPath) {
        return handleDropboxAction(() -> client.files().createFolderV2(folderPath), "Error creating folder");
    }

    @Override
    public FolderMetadata getFolderDetails(String folderPath) throws DropboxException {
        return getMetadata(folderPath, FolderMetadata.class, String.format("Error getting folder details: %s", folderPath));
    }

    @Override
    public FileMetadata getFileDetails(String filePath) throws DropboxException {
        return getMetadata(filePath, FileMetadata.class, String.format("Error getting file details: %s", filePath));
    }

    @Override
    public ListFolderResult listFolder(String folderPath, Boolean recursiveListing, Long limit) throws DropboxException {
        ListFolderBuilder listFolderBuilder = client.files().listFolderBuilder(folderPath);
        if (Objects.nonNull(recursiveListing)) {
            listFolderBuilder.withRecursive(recursiveListing);
        }
        if (Objects.nonNull(limit)) {
            listFolderBuilder.withLimit(limit);
        }

        return handleDropboxAction(listFolderBuilder::start, String.format("Error listing folder: %s", folderPath));
    }

    @Override
    public ListFolderResult listFolderContinue(String cursor) throws DropboxException {
        return handleDropboxAction(() -> client.files().listFolderContinue(cursor), "Error listing folder");
    }

    @Override
    public void deleteFile(String filePath) {
        handleDropboxAction(() -> client.files().deleteV2(filePath), String.format("Error deleting file: %s", filePath));
    }

    @Override
    public void deleteFolder(String folderPath) {
        handleDropboxAction(() -> client.files().deleteV2(folderPath), String.format("Error deleting folder: %s", folderPath));
    }

    private <T> T handleDropboxAction(DropboxActionResolver<T> action, String exceptionMessage) {
        try {
            return action.perform();
        } catch (Exception e) {
            String messageWithCause = String.format("%s with cause: %s", exceptionMessage, e.getMessage());
            throw new DropboxException(messageWithCause, e);
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T getMetadata(String path, Class<T> type, String message) {
        Metadata metadata = handleDropboxAction(() -> client.files().getMetadata(path),
                String.format("Error accessing details of: %s", path));

        checkIfMetadataIsInstanceOfGivenType(metadata, type, message);
        return (T) metadata;
    }

    private <T> void checkIfMetadataIsInstanceOfGivenType(Metadata metadata, Class<T> validType, String exceptionMessage) {
        boolean isValidType = validType.isInstance(metadata);
        if (!isValidType) {
            throw new DropboxException(exceptionMessage);
        }
    }
}

DropboxIntTest

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringBootSharepointRestApplication.class)
public class DropboxIntTest {

    private static final String TEST_FOLDER_PATH = "/Test Folder";
    private static final String TEST_FILE_PATH = String.format("%s/%s", TEST_FOLDER_PATH, "testFile.txt");
    private static final Integer TEST_FILE_SIZE = 17;

    @Autowired
    private DropboxService dropboxService;

    @Rule
    public final ExpectedException exceptions = ExpectedException.none();

    @Rule
    public final TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Before
    public void createTestFolder() throws Exception {
        dropboxService.createFolder(TEST_FOLDER_PATH);

        File tempUploadFile = temporaryFolder.newFile("testFile.txt");
        FileUtils.writeStringToFile(tempUploadFile, "test file content", "UTF-8");
        String testFilePath = String.format("%s/%s", TEST_FOLDER_PATH, "testFile.txt");
        dropboxService.uploadFile(testFilePath, new FileInputStream(tempUploadFile));
    }

    @After
    public void deleteTestFolder() {
        dropboxService.deleteFolder(TEST_FOLDER_PATH);
    }

    @Test
    public void downloadFile_shouldReturnNotEmptyInputStream() throws Exception {
        InputStream inputStream = dropboxService.downloadFile(TEST_FILE_PATH);
        assertThat(inputStream.available()).isEqualTo(TEST_FILE_SIZE);
    }

    @Test
    public void downloadFile_shouldThrowExceptionIfFileNotExists() {
        exceptions.expect(DropboxException.class);
        dropboxService.downloadFile("not-existing-file");
    }

    @Test
    public void uploadFile_shouldReturnUploadedFileDetails() throws Exception {
        File tempUploadFile = temporaryFolder.newFile("teamLogo.png");
        String filePath = String.format("%s/%s", TEST_FOLDER_PATH, tempUploadFile.getName());
        FileMetadata fileMetadata = dropboxService.uploadFile(filePath, new FileInputStream(tempUploadFile));
        assertThat(fileMetadata.getId()).isNotBlank();
    }

    @Test
    public void uploadFile_shouldCreateFolderIfNotExists() throws Exception {
        File tempUploadFile = temporaryFolder.newFile("teamLogo.png");
        String filePath = String.format("%s/%s/%s", TEST_FOLDER_PATH, "not existing folder", tempUploadFile.getName());
        dropboxService.uploadFile(filePath, new FileInputStream(tempUploadFile));

        FolderMetadata folderDetails = dropboxService
                .getFolderDetails(String.format("%s/%s", TEST_FOLDER_PATH, "not existing folder"));
        assertThat(folderDetails.getId()).isNotBlank();
    }

    @Test
    public void createFolder_shouldCreateFolder() {
        String folderPath = String.format("%s/%s", TEST_FOLDER_PATH, "new folder");
        CreateFolderResult folder = dropboxService.createFolder(folderPath);

        FolderMetadata folderDetails = dropboxService.getFolderDetails(folderPath);
        assertThat(folderDetails.getId()).isNotBlank();
        assertThat(folderDetails.getId()).isEqualToIgnoringCase(folder.getMetadata().getId());
    }

    @Test
    public void createFolder_shouldThrowExceptionIfFolderAlreadyExists() {
        String folderPath = String.format("%s/%s", TEST_FOLDER_PATH, "new folder");

        dropboxService.createFolder(folderPath);

        exceptions.expect(DropboxException.class);
        dropboxService.createFolder(folderPath);
    }

    @Test
    public void getFolderDetails_shouldReturnFolderDetails() {
        FolderMetadata folderDetails = dropboxService.getFolderDetails(TEST_FOLDER_PATH);

        assertThat(folderDetails.getId()).isNotBlank();
        assertThat(folderDetails.getName()).isNotBlank();
        assertThat(folderDetails.getPathDisplay()).isNotBlank();
    }

    @Test
    public void getFolderDetails_shouldThrowExceptionIfFolderNotExists() {
        exceptions.expect(DropboxException.class);
        dropboxService.getFolderDetails("/not existing folder");
    }

    @Test
    public void getFileDetails_shouldReturnFileDetails() {
        FileMetadata fileDetails = dropboxService.getFileDetails(TEST_FILE_PATH);

        assertThat(fileDetails.getId()).isNotBlank();
        assertThat(fileDetails.getPathDisplay()).isNotBlank();
        assertThat(fileDetails.getName()).isNotBlank();
        assertThat(fileDetails.getSize()).isEqualTo(TEST_FILE_SIZE.longValue());
        assertThat(fileDetails.getServerModified()).isNotNull();
        assertThat(fileDetails.getServerModified()).isBefore(new Date());
    }

    @Test
    public void getFileDetails_shouldThrowExceptionIfFileNotExists() {
        exceptions.expect(DropboxException.class);
        dropboxService.getFileDetails("/not-existing-file.pdf");
    }

    @Test
    public void listFolder_shouldReturnFolderItems() throws Exception {
        File tempUploadFile1 = temporaryFolder.newFile("testFile2.txt");
        FileUtils.writeStringToFile(tempUploadFile1, "test file content", "UTF-8");
        String testFilePath1 = String.format("%s/%s", TEST_FOLDER_PATH, "testFile2.txt");
        dropboxService.uploadFile(testFilePath1, new FileInputStream(tempUploadFile1));

        File tempUploadFile2 = temporaryFolder.newFile("testFile3.txt");
        FileUtils.writeStringToFile(tempUploadFile2, "test file content", "UTF-8");
        String testFilePath2 = String.format("%s/%s/%s", TEST_FOLDER_PATH, "inner folder", "testFile3.txt");
        dropboxService.uploadFile(testFilePath2, new FileInputStream(tempUploadFile2));

        ListFolderResult listFolderResult = dropboxService.listFolder(TEST_FOLDER_PATH, true, 10L);
        assertThat(listFolderResult.getEntries()).hasSize(5);

        List<FileMetadata> files = listFolderResult.getEntries().stream()
                .filter(entity -> entity instanceof FileMetadata)
                .map(entity -> (FileMetadata) entity)
                .collect(Collectors.toList());
        assertThat(files).hasSize(3);

        List<FolderMetadata> folders = listFolderResult.getEntries().stream()
                .filter(entity -> entity instanceof FolderMetadata)
                .map(entity -> (FolderMetadata) entity)
                .collect(Collectors.toList());
        assertThat(folders).hasSize(2);
    }

    @Test
    public void listFolder_shouldTrowExceptionIfFolderNotExists() {
        exceptions.expect(DropboxException.class);
        dropboxService.listFolder("/not existing folder", true, 10L);
    }

    @Test
    public void listFolderContinue_shouldListNextPathOfItems() throws Exception {
        File tempUploadFile = temporaryFolder.newFile("testFile2.txt");
        FileUtils.writeStringToFile(tempUploadFile, "test file content", "UTF-8");
        String testFilePath1 = String.format("%s/%s", TEST_FOLDER_PATH, "testFile2.txt");
        dropboxService.uploadFile(testFilePath1, new FileInputStream(tempUploadFile));

        ListFolderResult listFolderResult = dropboxService.listFolder(TEST_FOLDER_PATH, false, 1L);
        assertThat(listFolderResult.getEntries()).hasSize(1);

        String cursor = listFolderResult.getCursor();
        listFolderResult = dropboxService.listFolderContinue(cursor);
        assertThat(listFolderResult.getEntries()).hasSize(1);

        cursor = listFolderResult.getCursor();
        listFolderResult = dropboxService.listFolderContinue(cursor);
        assertThat(listFolderResult.getEntries()).hasSize(0);
    }

    @Test
    public void listFolderContinue_shouldThrowExceptionIfWrongCursorProvided() {
        exceptions.expect(DropboxException.class);
        dropboxService.listFolderContinue(UUID.randomUUID().toString());
    }

    @Test
    public void deleteFile_shouldDeleteFile() {
        FileMetadata fileDetails = dropboxService.getFileDetails(TEST_FILE_PATH);
        assertThat(fileDetails.getId()).isNotBlank();

        dropboxService.deleteFile(TEST_FILE_PATH);

        exceptions.expect(DropboxException.class);
        dropboxService.getFileDetails(TEST_FILE_PATH);
    }

    @Test
    public void deleteFile_shouldThrowExceptionIfFileNotExists() {
        exceptions.expect(DropboxException.class);
        dropboxService.deleteFolder("/not-existing-file");
    }

    @Test
    public void deleteFolder_shouldDeleteFolder() {
        String testFolder = String.format("%s/%s", TEST_FOLDER_PATH, "test folder");
        dropboxService.createFolder(testFolder);

        FolderMetadata folderDetails = dropboxService.getFolderDetails(testFolder);
        assertThat(folderDetails.getId()).isNotBlank();

        dropboxService.deleteFolder(testFolder);

        exceptions.expect(DropboxException.class);
        dropboxService.getFolderDetails(testFolder);
    }

    @Test
    public void deleteFolder_shouldThrowExceptionIfFolderNotExists() {
        exceptions.expect(DropboxException.class);
        dropboxService.deleteFolder("/not-existing-folder");
    }

}

DropboxException

public class DropboxException extends RuntimeException {

    public DropboxException(String message) {
        super(message);
    }

    public DropboxException(String message, Exception cause) {
        super(message, cause);
    }

}

DropboxActionResolver

@FunctionalInterface
interface DropboxActionResolver<T> {

    T perform() throws Exception;

}

What do you think of it?

\$\endgroup\$
3
\$\begingroup\$

For the most part it looks fine. There are a few parts that may be changed. For example, I think that the handle handleDropboxAction method and the usage of the DropboxActionResolver functional interface might be a bit of an overkill.

You could achieve the exact same behavior without creating a custom interface to essentially wrap it an try-catch clause.

listFolder arguments could be made primitive (to avoid the null checks).

Your various throws DropboxException declaration could be omitted.

\$\endgroup\$
  • \$\begingroup\$ Hey, thanks for your review. I will apply some of your suggestions. I used 'DropboxActionResolver' because some of Dropbox API methods throw checked exceptions and this can't be used with standard functional interfaces \$\endgroup\$ – Krzysztof Majewski Jul 30 '18 at 13:56
  • \$\begingroup\$ That make sense, but you can always create a wrapper exception object that will extend RuntimeException to propagate an exception outside of a lambda body. \$\endgroup\$ – Aris Jul 30 '18 at 14:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.