Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.3] - unreleased

### Changed

- `getMyFolders(int limit)` now reads your folders from `GET /v3/files/folders/home/list`, because the
endpoint it used before was retired by NDEx ([ndexbio/ndex-rest#163](https://github.com/ndexbio/ndex-rest/issues/163));
the method signature and return type are unchanged, so existing code still compiles.
- It now returns the folders at the top level of your home, most recently modified first, rather than
every folder at any depth ordered by name.
- Against NDEx servers older than 3.0.7 it still works and still returns at most `limit` folders, but
leaves each folder's `creationTime` unset.

## [3.0.2] - 2026-09-04

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import java.io.InputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
Expand All @@ -47,7 +49,9 @@
import org.ndexbio.model.exceptions.BadRequestException;
import org.ndexbio.model.exceptions.NdexException;
import org.ndexbio.model.object.CXSimplePathQuery;
import org.ndexbio.model.object.FileItemSummary;
import org.ndexbio.model.object.FileSearchResult;
import org.ndexbio.model.object.FileType;
import org.ndexbio.model.object.FileVisibilityType;
import org.ndexbio.model.object.MoveNetworksRequest;
import org.ndexbio.model.object.NdexFolder;
Expand Down Expand Up @@ -706,15 +710,72 @@ public void setNetworkSystemProperty(UUID networkId, Map<String,Object> properti


/**
* Lists the folders owned by the signed-in user.
* Lists the folders at the top level of the signed-in user's home, most recently modified first.
*
* <p>Returns at most {@code limit} folders; a non-positive {@code limit} returns an empty list.
* A full result is not proof that there are no further folders.</p>
*
* <p>Folders nested inside another folder are not included, and {@code owner} and
* {@code owner_id} are now filled in where they previously came back null.</p>
*
* <p>Against NDEx servers older than 3.0.7 each folder's {@code creationTime} is left unset;
* everything else is populated the same way on every supported server.</p>
*
* @param limit maximum number of folders to return
* @return the user's folders, newest-first as ordered by the server
* @return the user's top-level folders, most recently modified first
*/
public List<NdexFolder> getMyFolders(int limit)
throws JsonProcessingException, IOException, NdexException {
final String route = NdexApiVersion.v3 + "/files/folders?limit=" + limit;
return ndexRestClient.getNdexObjectList(route, "", NdexFolder.class);
if (limit <= 0)
return new ArrayList<>();

// size is spent on folders AND folder-targeted shortcuts, which are dropped below, so the
// server can return fewer than `limit` folders in a full page. Servers before 3.0.7 ignore
// size altogether. Both are why the result is trimmed here rather than trusted as-is.
final String route = NdexApiVersion.v3 + "/files/folders/home/list"
+ buildQuery("type", "folder", "format", "compact", "size", limit);
List<FileItemSummary> items =
ndexRestClient.getNdexObjectList(route, "", FileItemSummary.class);

List<NdexFolder> folders = new ArrayList<>();
for (FileItemSummary item : items) {
if (item == null || item.getType() != FileType.FOLDER)
continue;
folders.add(toFolder(item));
if (folders.size() == limit)
break;
}
return folders;
}

/** Maps one listing entry onto the folder representation this client has always returned. */
private static NdexFolder toFolder(FileItemSummary item) {
NdexFolder folder = new NdexFolder();
folder.setExternalId(item.getUuid());
folder.setName(item.getName());
folder.setModificationTime(item.getModificationTime());
folder.setOwner(item.getOwner());
folder.setOwner_id(item.getOwnerId() == null ? null : item.getOwnerId().toString());
folder.setParent(null); // these are the caller's top-level folders
folder.setIsDeleted(false); // a listing never includes deleted folders

// NdexExternalObject's constructor stamps creationTime with "now". Clear it, or a server that
// sends no creationTime (before 3.0.7) would hand back a fabricated timestamp instead of none.
folder.setCreationTime(null);

// Fields with no value are omitted from the response entirely, so every lookup below must
// tolerate an absent key, not merely a null value.
Map<String, Object> attributes = item.getAttributes();
if (attributes != null) {
Object description = attributes.get("description");
if (description != null)
folder.setDescription(description.toString());

Object created = attributes.get("creationTime");
if (created instanceof Number)
folder.setCreationTime(new Timestamp(((Number) created).longValue()));
}
return folder;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.UUID;

import org.easymock.Capture;
import org.easymock.EasyMock;
import org.junit.Test;
import org.ndexbio.model.object.FileItemSummary;
import org.ndexbio.model.object.FileSearchResult;
import org.ndexbio.model.object.FileType;
import org.ndexbio.model.object.FileVisibilityType;
import org.ndexbio.model.object.MoveNetworksRequest;
import org.ndexbio.model.object.NdexFolder;
Expand Down Expand Up @@ -63,22 +68,137 @@ public void buildQueryRejectsOddArgumentCount() {

// ---------- getMyFolders ----------

/** One listing entry as the server returns it, with attributes populated the compact view's way. */
private static FileItemSummary folderItem(UUID uuid, String name, Map<String, Object> attributes) {
FileItemSummary item = new FileItemSummary();
item.setUuid(uuid);
item.setType(FileType.FOLDER);
item.setName(name);
item.setAttributes(attributes);
return item;
}

@Test
public void getMyFoldersReadsTheHomeFolderListing() throws Exception {
NdexRestClient client = mock(NdexRestClient.class);
Capture<String> route = Capture.newInstance();
expect(client.getNdexObjectList(EasyMock.capture(route), eq(""), eq(FileItemSummary.class)))
.andReturn(Collections.singletonList(folderItem(FOLDER_ID, "My Project", null)));
replay(client);

List<NdexFolder> result = new NdexRestClientModelAccessLayer(client).getMyFolders(50);

assertEquals("v3/files/folders/home/list?type=folder&format=compact&size=50", route.getValue());
assertEquals(1, result.size());
assertEquals("My Project", result.get(0).getName());
assertEquals(FOLDER_ID, result.get(0).getExternalId());
verify(client);
}

@Test
public void getMyFoldersUsesV3RouteWithLimit() throws Exception {
public void getMyFoldersDropsFolderTargetedShortcuts() throws Exception {
// ?type=folder also returns shortcuts POINTING AT folders; they are not folders and must go.
FileItemSummary shortcut = new FileItemSummary();
shortcut.setUuid(UUID.randomUUID());
shortcut.setType(FileType.SHORTCUT);
shortcut.setName("link to a folder");

NdexRestClient client = mock(NdexRestClient.class);
NdexFolder folder = new NdexFolder();
folder.setName("My Project");
List<NdexFolder> folders = Collections.singletonList(folder);
expect(client.getNdexObjectList("v3/files/folders?limit=50", "", NdexFolder.class)).andReturn(folders);
expect(client.getNdexObjectList(anyObject(String.class), eq(""), eq(FileItemSummary.class)))
.andReturn(Arrays.asList(shortcut, folderItem(FOLDER_ID, "My Project", null)));
replay(client);

List<NdexFolder> result = new NdexRestClientModelAccessLayer(client).getMyFolders(50);

assertSame(folders, result);
assertEquals(1, result.size());
assertEquals("My Project", result.get(0).getName());
verify(client);
}

@Test
public void getMyFoldersReadsDescriptionAndCreationTimeFromAttributes() throws Exception {
long epoch = 1767312245000L;
Map<String, Object> attributes = new HashMap<>();
attributes.put("description", "a folder");
attributes.put("creationTime", epoch);

NdexRestClient client = mock(NdexRestClient.class);
expect(client.getNdexObjectList(anyObject(String.class), eq(""), eq(FileItemSummary.class)))
.andReturn(Collections.singletonList(folderItem(FOLDER_ID, "My Project", attributes)));
replay(client);

List<NdexFolder> result = new NdexRestClientModelAccessLayer(client).getMyFolders(50);

assertEquals("a folder", result.get(0).getDescription());
assertEquals(new Timestamp(epoch), result.get(0).getCreationTime());
verify(client);
}

@Test
public void getMyFoldersLeavesCreationTimeUnsetWhenServerOmitsIt() throws Exception {
// Servers older than 3.0.7 send no creationTime, and absent fields are omitted rather than
// sent as null -- so the lookup must degrade, not throw.
Map<String, Object> attributes = new HashMap<>();
attributes.put("description", "a folder");

NdexRestClient client = mock(NdexRestClient.class);
expect(client.getNdexObjectList(anyObject(String.class), eq(""), eq(FileItemSummary.class)))
.andReturn(Collections.singletonList(folderItem(FOLDER_ID, "My Project", attributes)));
replay(client);

List<NdexFolder> result = new NdexRestClientModelAccessLayer(client).getMyFolders(50);

assertNull(result.get(0).getCreationTime());
assertEquals("a folder", result.get(0).getDescription());
verify(client);
}

@Test
public void getMyFoldersToleratesAbsentAttributesMap() throws Exception {
NdexRestClient client = mock(NdexRestClient.class);
expect(client.getNdexObjectList(anyObject(String.class), eq(""), eq(FileItemSummary.class)))
.andReturn(Collections.singletonList(folderItem(FOLDER_ID, "My Project", null)));
replay(client);

List<NdexFolder> result = new NdexRestClientModelAccessLayer(client).getMyFolders(50);

assertNull(result.get(0).getCreationTime());
assertNull(result.get(0).getDescription());
verify(client);
}

@Test
public void getMyFoldersTrimsAnOverLongResponseToLimit() throws Exception {
// A server that ignores size (before 3.0.7) returns everything; the client still honours limit.
NdexRestClient client = mock(NdexRestClient.class);
expect(client.getNdexObjectList(anyObject(String.class), eq(""), eq(FileItemSummary.class)))
.andReturn(Arrays.asList(
folderItem(UUID.randomUUID(), "one", null),
folderItem(UUID.randomUUID(), "two", null),
folderItem(UUID.randomUUID(), "three", null)));
replay(client);

List<NdexFolder> result = new NdexRestClientModelAccessLayer(client).getMyFolders(2);

assertEquals(2, result.size());
assertEquals("one", result.get(0).getName());
assertEquals("two", result.get(1).getName());
verify(client);
}

@Test
public void getMyFoldersReturnsEmptyWithoutCallingTheServerWhenLimitNotPositive() throws Exception {
// The old route was a SQL LIMIT, so 0 meant "nothing". The listing treats a non-positive size as
// unbounded, so without the guard this would flip from returning nothing to returning everything.
NdexRestClient client = mock(NdexRestClient.class);
replay(client); // no calls expected

NdexRestClientModelAccessLayer layer = new NdexRestClientModelAccessLayer(client);
assertTrue(layer.getMyFolders(0).isEmpty());
assertTrue(layer.getMyFolders(-1).isEmpty());
verify(client);
}

// ---------- searchFiles ----------

@Test
Expand Down
Loading