Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8aa5fef
DefaultApplications: add constructor non-reactive params
Kehrlann Dec 16, 2025
6c4aa8f
DefaultApplications: remove parameter CloudFoundryClient and use this…
Kehrlann Dec 16, 2025
016e3c9
DefaultApplications: remove parameter spaceId and use this. instead
Kehrlann Dec 17, 2025
5e22e96
DefaultApplications: use applicationsV3 for routes
Kehrlann Dec 17, 2025
41e0d6c
DefaultApplications: use applicationsV3 to determine app started
Kehrlann Dec 18, 2025
b69b402
DefaultApplications: use process v3 for healtchecks
Kehrlann Dec 31, 2025
7b2553f
DefaultApplications: use audit events v3 for getEvents()
Kehrlann Jan 2, 2026
142ce60
DefaultApplications: logs use application v3
Kehrlann Jan 2, 2026
21b4430
DefaultApplications: rename uses v3 api
Kehrlann Jan 2, 2026
8986d79
tests: improve TestObjects.fill(...) to support interface params that…
Kehrlann Jan 2, 2026
15d243f
DefaultApplications: setHealthCheck uses v3 api
Kehrlann Jan 5, 2026
00f99c9
DefaultApplications: setEnvironmentVariable and unsetEnvironmentVaria…
Kehrlann Jan 6, 2026
0a30f77
DefaultApplications: remove unused methods
Kehrlann Jan 6, 2026
7041647
DefaultApplications: all stacks() references now use v3 API
Kehrlann Jan 8, 2026
5681319
ProcessState: Introduce STOPPING state
Kehrlann Jan 8, 2026
c09a203
DefaultApplications: migrate instance details & statistics to v3 API
Kehrlann Jan 9, 2026
b2d6758
DefaultApplications: use process statistics for instance count
Kehrlann Jan 12, 2026
dbcbea0
DefaultApplications: .get() uses v3 ApplicationResource for name, id,…
Kehrlann Jan 12, 2026
9b32880
DefaultApplications: use process statistics for disk and memory quota
Kehrlann Jan 12, 2026
d305454
DefaultApplications: refactor .get() into single method
Kehrlann Jan 13, 2026
e5b6f60
DefaultApplications: .get() does not use v2 summary anymore
Kehrlann Jan 13, 2026
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
Expand Up @@ -16,14 +16,15 @@

package org.cloudfoundry.client.v3.applications;

import java.util.Map;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.cloudfoundry.AllowNulls;
import org.immutables.value.Value;

import java.util.Map;

@JsonSerialize
@Value.Immutable
abstract class _UpdateApplicationEnvironmentVariablesRequest {
Expand All @@ -39,6 +40,7 @@ abstract class _UpdateApplicationEnvironmentVariablesRequest {
*/
@AllowNulls
@JsonProperty("var")
@JsonInclude(value = JsonInclude.Include.ALWAYS)
abstract Map<String, String> getVars();

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ public enum ProcessState {
/**
* The starting state
*/
STARTING("STARTING");
STARTING("STARTING"),

/**
* The stopping state
*/
STOPPING("STOPPING");

private final String value;

Expand All @@ -61,6 +66,8 @@ public static ProcessState from(String s) {
return RUNNING;
case "starting":
return STARTING;
case "stopping":
return STOPPING;
default:
throw new IllegalArgumentException(String.format("Unknown process state: %s", s));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public Advanced advanced() {
@Override
@Value.Derived
public Applications applications() {
return new DefaultApplications(getCloudFoundryClientPublisher(), getDopplerClientPublisher(), getSpaceId());
return new DefaultApplications(getCloudFoundryClient(), getDopplerClient(), getSpaceId().block());
}

@Override
Expand Down Expand Up @@ -318,12 +318,14 @@ private static boolean hasText(CharSequence str) {

private static Flux<OrganizationResource> requestOrganizations(Mono<CloudFoundryClient> cloudFoundryClientPublisher, String organization) {
return cloudFoundryClientPublisher
.flatMapMany(cloudFoundryClient -> PaginationUtils
.requestClientV3Resources(page -> cloudFoundryClient.organizationsV3()
.list(ListOrganizationsRequest.builder()
.name(organization)
.page(page)
.build())));
.flatMapMany(cloudFoundryClient -> PaginationUtils
.requestClientV3Resources(page -> cloudFoundryClient.organizationsV3()
.list(ListOrganizationsRequest.builder()
.name(organization)
.page(page)
.build())
)
);
}

private static Flux<SpaceResource> requestSpaces(Mono<CloudFoundryClient> cloudFoundryClientPublisher, String organizationId, String space) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public interface Applications {
Flux<ApplicationEvent> getEvents(GetApplicationEventsRequest request);

/**
* Retrieve the Health Check Type of an application
* Retrieve the Health Check Type of the web process of an application.
*
* @param request the get health check request
* @return the health check
Expand Down Expand Up @@ -225,7 +225,7 @@ public interface Applications {
Mono<Void> setEnvironmentVariable(SetEnvironmentVariableApplicationRequest request);

/**
* Set the Health Check Type of an application
* Set the Health Check Type of the web process of an application
*
* @param request the set health check request
* @return a completion indicator
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

package org.cloudfoundry.operations.applications;

import java.util.Date;

import org.cloudfoundry.Nullable;
import org.immutables.value.Value;

import java.util.Date;

/**
* Information about an instance of an application
*/
Expand Down Expand Up @@ -64,8 +64,11 @@ abstract class _InstanceDetail {

/**
* The time this instance was created
*
* @deprecated Always returns null. The "since" field is not returned by the CF v3 API.
*/
@Nullable
@Deprecated
abstract Date getSince();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
import org.cloudfoundry.client.v2.userprovidedserviceinstances.UserProvidedServiceInstances;
import org.cloudfoundry.client.v2.users.Users;
import org.cloudfoundry.client.v3.applications.ApplicationsV3;
import org.cloudfoundry.client.v3.auditevents.AuditEventsV3;
import org.cloudfoundry.client.v3.buildpacks.BuildpacksV3;
import org.cloudfoundry.client.v3.domains.DomainsV3;
import org.cloudfoundry.client.v3.jobs.JobsV3;
import org.cloudfoundry.client.v3.organizations.OrganizationsV3;
import org.cloudfoundry.client.v3.processes.Processes;
import org.cloudfoundry.client.v3.routes.RoutesV3;
import org.cloudfoundry.client.v3.spaces.SpacesV3;
import org.cloudfoundry.client.v3.stacks.StacksV3;
Expand Down Expand Up @@ -105,6 +107,7 @@ public abstract class AbstractOperationsTest {
protected final DopplerClient dopplerClient = mock(DopplerClient.class, RETURNS_SMART_NULLS);

protected final Events events = mock(Events.class, RETURNS_SMART_NULLS);
protected final AuditEventsV3 auditEventsV3 = mock(AuditEventsV3.class, RETURNS_SMART_NULLS);

protected final FeatureFlags featureFlags = mock(FeatureFlags.class, RETURNS_SMART_NULLS);

Expand All @@ -117,6 +120,7 @@ public abstract class AbstractOperationsTest {
protected final Organizations organizations = mock(Organizations.class, RETURNS_SMART_NULLS);
protected final OrganizationsV3 organizationsV3 =
mock(OrganizationsV3.class, RETURNS_SMART_NULLS);
protected final Processes processes = mock(Processes.class, RETURNS_SMART_NULLS);

protected final PrivateDomains privateDomains = mock(PrivateDomains.class, RETURNS_SMART_NULLS);

Expand Down Expand Up @@ -180,6 +184,7 @@ public final void mockClient() {
when(this.cloudFoundryClient.domains()).thenReturn(this.domains);
when(this.cloudFoundryClient.domainsV3()).thenReturn(this.domainsV3);
when(this.cloudFoundryClient.events()).thenReturn(this.events);
when(this.cloudFoundryClient.auditEventsV3()).thenReturn(this.auditEventsV3);
when(this.cloudFoundryClient.featureFlags()).thenReturn(this.featureFlags);
when(this.cloudFoundryClient.jobs()).thenReturn(this.jobs);
when(this.cloudFoundryClient.jobsV3()).thenReturn(this.jobsV3);
Expand All @@ -188,6 +193,7 @@ public final void mockClient() {
when(this.cloudFoundryClient.organizationQuotaDefinitions())
.thenReturn(this.organizationQuotaDefinitions);
when(this.cloudFoundryClient.privateDomains()).thenReturn(this.privateDomains);
when(this.cloudFoundryClient.processes()).thenReturn(this.processes);
when(this.cloudFoundryClient.resourceMatch()).thenReturn(this.resourceMatch);
when(this.cloudFoundryClient.routes()).thenReturn(this.routes);
when(this.cloudFoundryClient.routesV3()).thenReturn(this.routesV3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,45 @@
package org.cloudfoundry.operations;

import static org.assertj.core.api.Assertions.assertThat;

import static org.cloudfoundry.operations.TestObjects.fill;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import org.cloudfoundry.client.v3.organizations.ListOrganizationsResponse;
import org.cloudfoundry.client.v3.organizations.OrganizationResource;
import org.cloudfoundry.client.v3.spaces.ListSpacesResponse;
import org.cloudfoundry.client.v3.spaces.SpaceResource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;

final class DefaultCloudFoundryOperationsTest extends AbstractOperationsTest {

private final DefaultCloudFoundryOperations operations =
DefaultCloudFoundryOperations.builder()
.cloudFoundryClient(this.cloudFoundryClient)
.dopplerClient(this.dopplerClient)
.routingClient(this.routingClient)
.organization(TEST_ORGANIZATION_NAME)
.space(TEST_SPACE_NAME)
.uaaClient(this.uaaClient)
.build();
private DefaultCloudFoundryOperations operations;

@BeforeEach
void setUp() {
ListOrganizationsResponse orgsResponse =
fill(ListOrganizationsResponse.builder())
.resource(fill(OrganizationResource.builder()).build())
.build();
when(this.organizationsV3.list(any())).thenReturn(Mono.just(orgsResponse));
ListSpacesResponse spacesResponse =
fill(ListSpacesResponse.builder())
.resource(fill(SpaceResource.builder()).build())
.build();
when(this.spacesV3.list(any())).thenReturn(Mono.just(spacesResponse));

operations =
DefaultCloudFoundryOperations.builder()
.cloudFoundryClient(this.cloudFoundryClient)
.dopplerClient(this.dopplerClient)
.routingClient(this.routingClient)
.organization(TEST_ORGANIZATION_NAME)
.space(TEST_SPACE_NAME)
.uaaClient(this.uaaClient)
.build();
}

@Test
void advanced() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.cloudfoundry.operations;

import java.io.File;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;

class ReflectionUtils {

private ReflectionUtils() { // do not instantiate this class
}

/**
* Find implementations for a given interface type. Uses reflection.
*/
public static <T> List<Class<? extends T>> findImplementations(Class<T> interfaceType) {
try {
ClassLoader classLoader = interfaceType.getClassLoader();

String path = interfaceType.getPackage().getName().replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
ArrayList<URL> lr = Collections.list(resources);

return lr.stream()
.flatMap(
url -> {
if (url.getProtocol().equals("jar")) {
// Handle JAR URLs
return scanJar(
url,
interfaceType.getPackage().getName(),
interfaceType);
} else {
return scanDirectory(
new File(url.getFile()),
interfaceType.getPackage().getName(),
interfaceType);
}
})
.collect(Collectors.toList());
} catch (Exception ignored) {

}
return Collections.emptyList();
}

/**
* Find implementations for the given interface type in a source directory.
*/
private static <T> Stream<Class<? extends T>> scanDirectory(
File directory, String packageName, Class<T> interfaceType) {
File[] files = directory.listFiles();
if (files == null) {
return Stream.empty();
}

Stream<Class<? extends T>> classes =
Arrays.stream(files)
.filter(fileName -> fileName.getName().endsWith(".class"))
.map(
fileName ->
packageName
+ '.'
+ fileName.getName().replaceAll("\\.class$", ""))
.<Class<? extends T>>map(
className ->
getClassIfImplementsInterface(className, interfaceType))
.filter(Objects::nonNull);
Stream<Class<? extends T>> directories =
Arrays.stream(files)
.filter(File::isDirectory)
.flatMap(
fileName ->
scanDirectory(
fileName,
packageName + "." + fileName.getName(),
interfaceType));
return Stream.concat(classes, directories);
}

/**
* Find implementations for the given interface type in a packaged jar.
* When running {@code mvn package}, class files are packaged in jar files,
* and is not available directly on the filesystem.
*/
private static <T> Stream<Class<? extends T>> scanJar(
URL jarUrl, String packageName, Class<T> interfaceType) {
try {
JarURLConnection jarConnection = (JarURLConnection) jarUrl.openConnection();
JarFile jarFile = jarConnection.getJarFile();
String packagePath = packageName.replace('.', '/');

return jarFile.stream()
.filter(
entry -> {
String name = entry.getName();
return name.startsWith(packagePath)
&& name.endsWith(".class")
&& !name.equals(packagePath + ".class");
})
.map(entry -> entry.getName().replace('/', '.').replaceAll("\\.class$", ""))
.<Class<? extends T>>map(
className -> getClassIfImplementsInterface(className, interfaceType))
.filter(Objects::nonNull);
} catch (Exception e) {
return Stream.empty();
}
}

/**
* Return the {@link Class} instance for {@code className}, if it implements {@code interfaceType}. Otherwise, return null.
*/
private static <T> Class<? extends T> getClassIfImplementsInterface(
String className, Class<T> interfaceType) {
try {
Class<?> clazz = Class.forName(className);
if (interfaceType.isAssignableFrom(clazz)
&& !clazz.isInterface()
&& !Modifier.isAbstract(clazz.getModifiers())) {
Class<? extends T> subclass = clazz.asSubclass(interfaceType);
return subclass;
}
} catch (ClassNotFoundException ignored) {
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,12 @@ private static <T> T fill(T builder, Optional<String> modifier) {
return getConfigurationMethods(builderType, builderMethods, builtGetters).stream()
.collect(
() -> builder,
(b, method) ->
ReflectionUtils.invokeMethod(
method, b, getConfiguredValue(method, modifier)),
(b, method) -> {
Object configuredValue = getConfiguredValue(method, modifier);
if (configuredValue != null) {
ReflectionUtils.invokeMethod(method, b, configuredValue);
}
},
(a, b) -> {});
}

Expand Down Expand Up @@ -190,10 +193,19 @@ private static Object getConfiguredValue(
return getConfiguredString(configurationMethod, modifier);
} else if (parameterType.isArray()) {
return Array.newInstance(parameterType.getComponentType(), 0);
} else if (parameterType == Map.Entry.class) {
return null;
} else {
throw new IllegalStateException(
String.format("Unable to configure %s", configurationMethod));
for (Class<?> implementation :
org.cloudfoundry.operations.ReflectionUtils.findImplementations(
parameterType)) {
if (isBuiltType(implementation)) {
return getConfiguredBuilder(implementation, modifier);
}
}
}
throw new IllegalStateException(
String.format("Unable to configure %s", configurationMethod));
}

private static List<Method> getMethods(Class<?> builderType) {
Expand Down
Loading