diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0da7299 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + unit-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: temurin + cache: maven + + - name: Build and unit test + run: mvn --batch-mode test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6b77003 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + environment: MVN + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: 'maven' + + - name: Extract and validate version from tag + id: version + run: | + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "Error: '$VERSION' is not valid semver (expected X.Y.Z)" + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set version in pom.xml + run: mvn versions:set -DnewVersion=${{ steps.version.outputs.version }} -DgenerateBackupPoms=false + + - name: Build release JAR + run: mvn package -DskipTests + + - name: Upload JAR to release + run: | + gh release upload "${{ github.event.release.tag_name }}" \ + "target/ndex-java-client-${{ steps.version.outputs.version }}.jar" \ + --clobber + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to Maven repository + run: mvn deploy -DskipTests --settings .mvn/settings.xml + env: + REPO_USER: ${{ secrets.REPO_USER }} + REPO_PWD: ${{ secrets.REPO_PWD }} diff --git a/.mvn/settings.xml b/.mvn/settings.xml new file mode 100644 index 0000000..246530a --- /dev/null +++ b/.mvn/settings.xml @@ -0,0 +1,14 @@ + + + + cytoscape_releases + ${env.REPO_USER} + ${env.REPO_PWD} + + + cytoscape_snapshots + ${env.REPO_USER} + ${env.REPO_PWD} + + + diff --git a/.travis.settings.xml b/.travis.settings.xml deleted file mode 100644 index e817c64..0000000 --- a/.travis.settings.xml +++ /dev/null @@ -1,142 +0,0 @@ - - - - - releases - ${env.NEXUS_USERNAME} - ${env.NEXUS_PASSWORD} - - - snapshots - ${env.NEXUS_USERNAME} - ${env.NEXUS_PASSWORD} - - - thirdparty - ${env.NEXUS_USERNAME} - ${env.NEXUS_PASSWORD} - - - - - standard-with-extra-repos - - true - - - - cytoscape_snapshots - - true - - - false - - Cytoscape Snapshots - ${env.NEXUS_SNAPSHOT_URL} - - - cytoscape_releases - - false - - - true - - Cytoscape Releases - http://code.cytoscape.org/nexus/content/repositories/releases/ - - - cytoscape_thirdparty - - false - - - true - - Cytoscape Third Party - http://code.cytoscape.org/nexus/content/repositories/thirdparty/ - - - central - Central Repository - http://repo.maven.apache.org/maven2 - - true - - - false - - - - sonatype - OSS Sonatype repo (releases) - - true - always - warn - - - false - never - fail - - https://oss.sonatype.org/content/repositories/releases/ - - - sonatype-snapshots - OSS Sonatype repo (snapshots) - - false - always - warn - - - true - never - fail - - https://oss.sonatype.org/content/repositories/snapshots/ - - - sonatype-apache - Apache repo (releases) - - true - always - warn - - - false - never - fail - - https://repository.apache.org/content/repositories/releases/ - - - apache-snapshots - ASF repo (snapshots) - - false - never - warn - - - true - always - fail - - https://repository.apache.org/snapshots/ - - - - - - - google-maven-central - GCS Maven Central mirror - https://maven-central.storage-download.googleapis.com/repos/central/data/ - central - - - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 55ea7d1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: java - -sudo: false - -jdk: - - openjdk11 - -branches: - only: - master - -before_script: - - cp src/test/resources/ndex-server.properties.sample src/test/resources/ndex-server.properties - - sed -i 's/PLACEHOLDER_HOST/'"$SERVER_HOST"'/g' src/test/resources/ndex-server.properties - - sed -i 's/PLACEHOLDER_USERNAME/'"$SERVER_USERNAME"'/g' src/test/resources/ndex-server.properties - - sed -i 's/PLACEHOLDER_PASSWORD/'"$SERVER_PASSWORD"'/g' src/test/resources/ndex-server.properties - -# deploy: -# provider: script -# script: "cp .travis.settings.xml $HOME/.m2/settings.xml && mvn deploy" -# skip_cleanup: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5d5a393 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +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.1] - 2026-09-03 + +### Added + +- **NDEx v3 folder support** — `getMyFolders(int limit)` lists the signed-in user's folders via `GET /v3/files/folders`, and `moveNetworks(MoveNetworksRequest)` moves networks into a folder via `POST /v3/batch/networks/move`. +- **NDEx v3 file search** — `searchFiles(SimpleFileQuery, FileVisibilityType, int start, int size)` queries `POST /v3/search/files`, supporting an optional `PUBLIC`/`PRIVATE` visibility filter and paging, and returning a `FileSearchResult`. +- **Visibility and folder on CX2 network create** — `createCX2Network(InputStream, VisibilityType, UUID folderId)` adds the `visibility` and `folderId` query parameters to `POST /v3/networks`, so a network can be created directly into a folder with its visibility set. A null value omits its parameter. +- **Visibility on CX2 network update** — `updateCX2Network(UUID, InputStream, VisibilityType)` adds the `visibility` query parameter to `PUT /v3/networks/{networkid}`. A null value omits it. + +The pre-existing single-argument `createCX2Network(InputStream)` and `updateCX2Network(UUID, InputStream)` now delegate to the new overloads with null arguments and produce exactly the same unqueried routes as before, so existing callers are unaffected. + +### Removed + +- **Travis CI configuration** — deleted `.travis.yml` and `.travis.settings.xml` and dropped the Travis build-status badge from `README.md`. Replaced by GitHub Actions `ci.yml` and `release.yml` workflows, the latter deploying to the NRNB Nexus repository on a published GitHub release. diff --git a/Makefile b/Makefile index 7c76317..52f9425 100644 --- a/Makefile +++ b/Makefile @@ -43,5 +43,10 @@ coverage: ## check code coverage with jacoco install: clean ## install the package to local repo mvn install +deploy: ## deploy to nrnb nexus, requires VERSION=x.y.z[-SNAPSHOT] + @test -n "$(VERSION)" || (echo "VERSION is required, e.g. make deploy VERSION=3.0.1-SNAPSHOT" && exit 1) + mvn versions:set -DnewVersion=$(VERSION) -DgenerateBackupPoms=false + mvn deploy + updateversion: ## updates version in pom.xml via maven command mvn versions:set diff --git a/README.md b/README.md index e3d3182..dbaa978 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ # ndex-java-client -[![Build Status](https://app.travis-ci.com/ndexbio/ndex-java-client.svg?branch=master)](https://app.travis-ci.com/ndexbio/ndex-java-client) - Java client library for accessing NDEx REST server diff --git a/pom.xml b/pom.xml index 93b3490..5ba99a5 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.ndexbio.client ndex-java-client - 3.0.0 + 3.0.1 NDEx Java REST Client REST Client for in applications that access NDEx 2013 diff --git a/src/main/java/org/ndexbio/rest/client/NdexRestClient.java b/src/main/java/org/ndexbio/rest/client/NdexRestClient.java index 40c197f..5d005f7 100644 --- a/src/main/java/org/ndexbio/rest/client/NdexRestClient.java +++ b/src/main/java/org/ndexbio/rest/client/NdexRestClient.java @@ -595,6 +595,27 @@ private HttpURLConnection putReturningConnection(final String route, */ + /** + * POSTs a JSON body to a route that returns no content of interest, validating the + * response status the same way {@link #putNdexObject(String, JsonNode)} does. + */ + protected void postNdexObjectNoContent( + final String route, + final JsonNode postData) + throws IllegalStateException, Exception { + HttpURLConnection con = null; + try { + con = postReturningConnection(route, postData); + if ( con.getResponseCode() != HttpURLConnection.HTTP_NO_CONTENT && + con.getResponseCode() != HttpURLConnection.HTTP_OK && + con.getResponseCode() != HttpURLConnection.HTTP_CREATED) { + processNdexSpecificException(con.getErrorStream(), con.getResponseCode(), new ObjectMapper()); + } + } finally { + if ( con != null) con.disconnect(); + } + } + protected T postNdexObject( final String route, final JsonNode postData, diff --git a/src/main/java/org/ndexbio/rest/client/NdexRestClientModelAccessLayer.java b/src/main/java/org/ndexbio/rest/client/NdexRestClientModelAccessLayer.java index 02088bd..da7bb1c 100644 --- a/src/main/java/org/ndexbio/rest/client/NdexRestClientModelAccessLayer.java +++ b/src/main/java/org/ndexbio/rest/client/NdexRestClientModelAccessLayer.java @@ -47,6 +47,11 @@ import org.ndexbio.model.exceptions.BadRequestException; import org.ndexbio.model.exceptions.NdexException; import org.ndexbio.model.object.CXSimplePathQuery; +import org.ndexbio.model.object.FileSearchResult; +import org.ndexbio.model.object.FileVisibilityType; +import org.ndexbio.model.object.MoveNetworksRequest; +import org.ndexbio.model.object.NdexFolder; +import org.ndexbio.model.object.SimpleFileQuery; import org.ndexbio.model.object.Group; import org.ndexbio.model.object.NdexPropertyValuePair; import org.ndexbio.model.object.NdexStatus; @@ -58,6 +63,7 @@ import org.ndexbio.model.object.Status; import org.ndexbio.model.object.Task; import org.ndexbio.model.object.User; +import org.ndexbio.model.object.network.VisibilityType; import org.ndexbio.model.object.network.NetworkSummary; import com.fasterxml.jackson.core.JsonProcessingException; @@ -699,12 +705,85 @@ public void setNetworkSystemProperty(UUID networkId, Map properti } + /** + * Lists the folders owned by the signed-in user. + * + * @param limit maximum number of folders to return + * @return the user's folders, newest-first as ordered by the server + */ + public List getMyFolders(int limit) + throws JsonProcessingException, IOException, NdexException { + final String route = NdexApiVersion.v3 + "/files/folders?limit=" + limit; + return ndexRestClient.getNdexObjectList(route, "", NdexFolder.class); + } + + /** + * Searches files (networks, folders and shortcuts) visible to the caller. + * + * @param query the search criteria; a null query searches with no criteria + * @param visibility restricts results to this visibility, or null for no restriction + * @param start index of the first result to return + * @param size maximum number of results to return + * @return the matching files together with the total number found + */ + public FileSearchResult searchFiles(SimpleFileQuery query, FileVisibilityType visibility, int start, int size) + throws JsonProcessingException, IOException, NdexException { + final String route = NdexApiVersion.v3 + "/search/files?start=" + start + "&size=" + size + + (visibility == null ? "" : "&visibility=" + visibility); + JsonNode postData = objectMapper.valueToTree(query == null ? new SimpleFileQuery() : query); + return ndexRestClient.postNdexObject(route, postData, FileSearchResult.class); + } + + /** + * Moves networks into a target folder. + * + * @param request the networks to move and the folder to move them into + */ + public void moveNetworks(MoveNetworksRequest request) + throws IllegalStateException, Exception { + final String route = NdexApiVersion.v3 + "/batch/networks/move"; + ndexRestClient.postNdexObjectNoContent(route, objectMapper.valueToTree(request)); + } + public UUID createCXNetwork (InputStream input) throws IllegalStateException, Exception { return createNetwork(input, NdexApiVersion.v2 + "/network", txtAcceptJsonContentRequestProperties).getUuid(); } public UUID createCX2Network (InputStream input) throws IllegalStateException, Exception { - return createNetwork(input, NdexApiVersion.v3 + "/networks", jsonAcceptContentRequestProperties).getUuid(); + return createCX2Network(input, null, null); + } + + /** + * Creates a CX2 network on the server, optionally setting its visibility and placing it in a folder. + * + * @param input the CX2 network stream + * @param visibility visibility to apply on creation, or null to leave the server default + * @param folderId UUID of the folder to create the network in, or null for the user's root + * @return the UUID of the newly created network + */ + public UUID createCX2Network (InputStream input, VisibilityType visibility, UUID folderId) + throws IllegalStateException, Exception { + final String route = NdexApiVersion.v3 + "/networks" + + buildQuery("visibility", visibility, "folderId", folderId); + return createNetwork(input, route, jsonAcceptContentRequestProperties).getUuid(); + } + + /** + * Builds a query string from alternating name/value pairs, skipping any pair whose value is null. + * Returns the empty string when no value survives. + */ + static String buildQuery(Object... nameValuePairs) { + if (nameValuePairs.length % 2 != 0) + throw new IllegalArgumentException("expected alternating name/value pairs"); + StringBuilder query = new StringBuilder(); + for (int i = 0; i < nameValuePairs.length; i += 2) { + Object value = nameValuePairs[i + 1]; + if (value == null) + continue; + query.append(query.length() == 0 ? "?" : "&") + .append(nameValuePairs[i]).append("=").append(value); + } + return query.toString(); } private NdexObjectUpdateStatus createNetwork(InputStream input, final String route, @@ -786,11 +865,28 @@ public void updateCXNetwork (UUID networkUUID, InputStream input) throws Illegal } public void updateCX2Network (UUID networkUUID, InputStream input) throws IllegalStateException, Exception { - updateNetwork (networkUUID, input,NdexApiVersion.v3 + "/networks"); - } + updateCX2Network (networkUUID, input, null); + } + + /** + * Replaces a CX2 network on the server, optionally changing its visibility. + * + * @param networkUUID the network to replace + * @param input the CX2 network stream + * @param visibility visibility to apply, or null to leave it unchanged + */ + public void updateCX2Network (UUID networkUUID, InputStream input, VisibilityType visibility) + throws IllegalStateException, Exception { + updateNetwork (networkUUID, input, NdexApiVersion.v3 + "/networks", + buildQuery("visibility", visibility)); + } private void updateNetwork(UUID networkUUID, InputStream input, final String route) throws IllegalStateException, Exception { - HttpURLConnection con = ndexRestClient.createReturningConnection(route + "/" + networkUUID.toString(), + updateNetwork(networkUUID, input, route, ""); + } + + private void updateNetwork(UUID networkUUID, InputStream input, final String route, final String query) throws IllegalStateException, Exception { + HttpURLConnection con = ndexRestClient.createReturningConnection(route + "/" + networkUUID.toString() + query, input, "PUT", jsonAcceptContentRequestProperties); if (con.getResponseCode() != HttpURLConnection.HTTP_NO_CONTENT){ diff --git a/src/test/java/org/ndexbio/rest/client/NdexRestClientModelAccessLayerV3Test.java b/src/test/java/org/ndexbio/rest/client/NdexRestClientModelAccessLayerV3Test.java new file mode 100644 index 0000000..d71dd8b --- /dev/null +++ b/src/test/java/org/ndexbio/rest/client/NdexRestClientModelAccessLayerV3Test.java @@ -0,0 +1,278 @@ +package org.ndexbio.rest.client; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.mock; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +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.FileSearchResult; +import org.ndexbio.model.object.FileVisibilityType; +import org.ndexbio.model.object.MoveNetworksRequest; +import org.ndexbio.model.object.NdexFolder; +import org.ndexbio.model.object.SimpleFileQuery; +import org.ndexbio.model.object.network.VisibilityType; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Covers the NDEx v3 folder, file-search and CX2 network methods, asserting the routes and + * query strings they build and the objects they map back. + */ +public class NdexRestClientModelAccessLayerV3Test { + + private static final UUID NETWORK_ID = UUID.fromString("e2ae10b4-dfba-4a15-bdb1-91c2257e12ac"); + private static final UUID FOLDER_ID = UUID.fromString("11111111-2222-3333-4444-555555555555"); + + // ---------- buildQuery ---------- + + @Test + public void buildQueryOmitsNullValues() { + assertEquals("", NdexRestClientModelAccessLayer.buildQuery("visibility", null, "folderId", null)); + assertEquals("?visibility=PUBLIC", + NdexRestClientModelAccessLayer.buildQuery("visibility", VisibilityType.PUBLIC, "folderId", null)); + assertEquals("?folderId=" + FOLDER_ID, + NdexRestClientModelAccessLayer.buildQuery("visibility", null, "folderId", FOLDER_ID)); + assertEquals("?visibility=PRIVATE&folderId=" + FOLDER_ID, + NdexRestClientModelAccessLayer.buildQuery("visibility", VisibilityType.PRIVATE, "folderId", FOLDER_ID)); + } + + @Test(expected = IllegalArgumentException.class) + public void buildQueryRejectsOddArgumentCount() { + NdexRestClientModelAccessLayer.buildQuery("visibility"); + } + + // ---------- getMyFolders ---------- + + @Test + public void getMyFoldersUsesV3RouteWithLimit() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + NdexFolder folder = new NdexFolder(); + folder.setName("My Project"); + List folders = Collections.singletonList(folder); + expect(client.getNdexObjectList("v3/files/folders?limit=50", "", NdexFolder.class)).andReturn(folders); + replay(client); + + List result = new NdexRestClientModelAccessLayer(client).getMyFolders(50); + + assertSame(folders, result); + assertEquals("My Project", result.get(0).getName()); + verify(client); + } + + // ---------- searchFiles ---------- + + @Test + public void searchFilesOmitsVisibilityWhenNull() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + FileSearchResult expected = new FileSearchResult(0L, 0L, Collections.emptyList()); + expect(client.postNdexObject(eq("v3/search/files?start=0&size=100"), anyObject(JsonNode.class), + eq(FileSearchResult.class))).andReturn(expected); + replay(client); + + FileSearchResult result = new NdexRestClientModelAccessLayer(client).searchFiles(null, null, 0, 100); + + assertSame(expected, result); + verify(client); + } + + @Test + public void searchFilesAppendsVisibilityAndSerializesQuery() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + Capture body = EasyMock.newCapture(); + FileSearchResult expected = new FileSearchResult(3L, 10L, Collections.emptyList()); + expect(client.postNdexObject(eq("v3/search/files?start=10&size=25&visibility=PRIVATE"), + EasyMock.capture(body), eq(FileSearchResult.class))).andReturn(expected); + replay(client); + + SimpleFileQuery query = new SimpleFileQuery(); + query.setSearchString("cancer signaling"); + query.setAccountName("alice"); + FileSearchResult result = new NdexRestClientModelAccessLayer(client) + .searchFiles(query, FileVisibilityType.PRIVATE, 10, 25); + + assertEquals(3L, result.getNumFound()); + assertEquals("cancer signaling", body.getValue().get("searchString").asText()); + assertEquals("alice", body.getValue().get("accountName").asText()); + verify(client); + } + + @Test + public void searchFilesAcceptsPublicVisibility() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + expect(client.postNdexObject(eq("v3/search/files?start=0&size=5&visibility=PUBLIC"), + anyObject(JsonNode.class), eq(FileSearchResult.class))) + .andReturn(new FileSearchResult(0L, 0L, Collections.emptyList())); + replay(client); + + new NdexRestClientModelAccessLayer(client).searchFiles(new SimpleFileQuery(), FileVisibilityType.PUBLIC, 0, 5); + + verify(client); + } + + // ---------- moveNetworks ---------- + + @Test + public void moveNetworksPostsBatchRoute() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + Capture body = EasyMock.newCapture(); + client.postNdexObjectNoContent(eq("v3/batch/networks/move"), EasyMock.capture(body)); + expectLastCall(); + replay(client); + + MoveNetworksRequest request = new MoveNetworksRequest(); + request.setTargetFolder(FOLDER_ID); + request.setNetworks(Arrays.asList(NETWORK_ID)); + new NdexRestClientModelAccessLayer(client).moveNetworks(request); + + assertEquals(FOLDER_ID.toString(), body.getValue().get("targetFolder").asText()); + assertEquals(NETWORK_ID.toString(), body.getValue().get("networks").get(0).asText()); + verify(client); + } + + // ---------- createCX2Network ---------- + + @Test + public void createCX2NetworkAppliesVisibilityAndFolder() throws Exception { + assertEquals("v3/networks?visibility=PUBLIC&folderId=" + FOLDER_ID, + captureCreateRoute(VisibilityType.PUBLIC, FOLDER_ID)); + } + + @Test + public void createCX2NetworkAppliesVisibilityOnly() throws Exception { + assertEquals("v3/networks?visibility=PRIVATE", captureCreateRoute(VisibilityType.PRIVATE, null)); + } + + @Test + public void createCX2NetworkAppliesFolderOnly() throws Exception { + assertEquals("v3/networks?folderId=" + FOLDER_ID, captureCreateRoute(null, FOLDER_ID)); + } + + @Test + public void createCX2NetworkWithNeitherSendsNoQuery() throws Exception { + assertEquals("v3/networks", captureCreateRoute(null, null)); + } + + @Test + public void createCX2NetworkSingleArgOverloadIsUnqueried() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + Capture route = EasyMock.newCapture(); + expect(client.createReturningConnection(EasyMock.capture(route), anyObject(InputStream.class), eq("POST"), + anyObject())).andReturn(createdConnection(NETWORK_ID)); + replay(client); + + UUID id = new NdexRestClientModelAccessLayer(client).createCX2Network(cx2Stream()); + + assertEquals("v3/networks", route.getValue()); + assertEquals(NETWORK_ID, id); + verify(client); + } + + private String captureCreateRoute(VisibilityType visibility, UUID folderId) throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + Capture route = EasyMock.newCapture(); + expect(client.createReturningConnection(EasyMock.capture(route), anyObject(InputStream.class), eq("POST"), + anyObject())).andReturn(createdConnection(NETWORK_ID)); + replay(client); + + UUID id = new NdexRestClientModelAccessLayer(client).createCX2Network(cx2Stream(), visibility, folderId); + + assertEquals(NETWORK_ID, id); + verify(client); + return route.getValue(); + } + + // ---------- updateCX2Network ---------- + + @Test + public void updateCX2NetworkAppendsVisibility() throws Exception { + assertEquals("v3/networks/" + NETWORK_ID + "?visibility=PUBLIC", + captureUpdateRoute(VisibilityType.PUBLIC)); + } + + @Test + public void updateCX2NetworkOmitsVisibilityWhenNull() throws Exception { + assertEquals("v3/networks/" + NETWORK_ID, captureUpdateRoute(null)); + } + + @Test + public void updateCX2NetworkSingleArgOverloadIsUnqueried() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + Capture route = EasyMock.newCapture(); + expect(client.createReturningConnection(EasyMock.capture(route), anyObject(InputStream.class), eq("PUT"), + anyObject())).andReturn(noContentConnection()); + replay(client); + + new NdexRestClientModelAccessLayer(client).updateCX2Network(NETWORK_ID, cx2Stream()); + + assertEquals("v3/networks/" + NETWORK_ID, route.getValue()); + verify(client); + } + + @Test + public void updateCXNetworkStillUsesV2Route() throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + Capture route = EasyMock.newCapture(); + expect(client.createReturningConnection(EasyMock.capture(route), anyObject(InputStream.class), eq("PUT"), + anyObject())).andReturn(noContentConnection()); + replay(client); + + new NdexRestClientModelAccessLayer(client).updateCXNetwork(NETWORK_ID, cx2Stream()); + + assertEquals("v2/network/" + NETWORK_ID, route.getValue()); + verify(client); + } + + private String captureUpdateRoute(VisibilityType visibility) throws Exception { + NdexRestClient client = mock(NdexRestClient.class); + Capture route = EasyMock.newCapture(); + expect(client.createReturningConnection(EasyMock.capture(route), anyObject(InputStream.class), eq("PUT"), + anyObject())).andReturn(noContentConnection()); + replay(client); + + new NdexRestClientModelAccessLayer(client).updateCX2Network(NETWORK_ID, cx2Stream(), visibility); + + verify(client); + return route.getValue(); + } + + // ---------- helpers ---------- + + private static InputStream cx2Stream() { + return new ByteArrayInputStream("[{\"CXVersion\":\"2.0\"}]".getBytes(StandardCharsets.UTF_8)); + } + + private static HttpURLConnection createdConnection(UUID newId) throws Exception { + HttpURLConnection con = mock(HttpURLConnection.class); + expect(con.getResponseCode()).andReturn(HttpURLConnection.HTTP_CREATED).anyTimes(); + expect(con.getInputStream()).andReturn(new ByteArrayInputStream( + ("{\"uuid\":\"" + newId + "\"}").getBytes(StandardCharsets.UTF_8))).anyTimes(); + replay(con); + return con; + } + + private static HttpURLConnection noContentConnection() throws Exception { + HttpURLConnection con = mock(HttpURLConnection.class); + expect(con.getResponseCode()).andReturn(HttpURLConnection.HTTP_NO_CONTENT).anyTimes(); + replay(con); + return con; + } +}