8000 Client streaming for admin api v2 by shawkins · Pull Request #47611 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,23 @@

import jakarta.ws.rs.client.ClientBuilder;

public class ClientBuilderWrapper {
import org.keycloak.admin.client.spi.ResteasyClientClassicProvider;

static Class clazz;
static {
try {
clazz = Class.forName("org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl");
} catch (ClassNotFoundException e) {
try {
clazz = Class.forName("org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder");
} catch (ClassNotFoundException ex) {
throw new RuntimeException("RestEasy 3 or 4 not found on classpath");
}
}
}
import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl;

/**
* Use {@link ResteasyClientClassicProvider#createClientBuilder()} instead
*/
@Deprecated
public class ClientBuilderWrapper {

public static ClientBuilder create(SSLContext sslContext, boolean disableTrustManager) {
try {
Object o = clazz.newInstance();
clazz.getMethod("sslContext", SSLContext.class).invoke(o, sslContext);
clazz.getMethod("connectionPoolSize", int.class).invoke(o, 10);
if (disableTrustManager) {
clazz.getMethod("disableTrustManager").invoke(o);
}
return (ClientBuilder) o;
} catch (Exception e) {
throw new RuntimeException(e);
ResteasyClientBuilderImpl result = ResteasyClientClassicProvider.createClientBuilder();
result.sslContext(sslContext);
if (disableTrustManager) {
result.disableTrustManager();
}
return result;
}

}
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
package org.keycloak.admin.client;

import java.util.stream.Stream;

import jakarta.ws.rs.core.MediaType;

import org.keycloak.admin.client.deserializer.StreamDeserializer;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider;

public class JacksonProvider extends ResteasyJackson2Provider {
private static final StreamDeserializer STREAM_DESERIALIZER = new StreamDeserializer();
private static final SimpleModule STREAM_MODULE = new SimpleModule().addDeserializer(Stream.class, STREAM_DESERIALIZER);

@Override
public ObjectMapper locateMapper(Class<?> type, MediaType mediaType) {
ObjectMapper objectMapper = super.locateMapper(type, mediaType);

// Same like JSONSerialization class. Makes it possible to use admin-client against older versions of Keycloak server where the properties on representations might be different
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);

// The client must work with the newer versions of Keycloak server, which might contain the JSON fields not yet known by the client. So unknown fields will be ignored.
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

objectMapper.registerModule(STREAM_MODULE);
return objectMapper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* .password("pass")
* .clientId("client")
* .clientSecret("secret")
* .resteasyClient(new ResteasyClientBuilderImpl()
* .resteasyClient(ResteasyClientClassicProvider.createClientBuilder()
* .connectionPoolSize(20)
* .build()
* .register(org.keycloak.admin.client.JacksonProvider.class, 100))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@
import javax.net.ssl.SSLContext;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.WebTarget;

import org.keycloak.admin.client.ClientBuilderWrapper;
import org.keycloak.admin.client.JacksonProvider;

import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl;

/**
* An implementation of {@link ResteasyClientProvider} based on RESTEasy classic.
Expand All @@ -35,15 +34,26 @@ public class ResteasyClientClassicProvider implements ResteasyClientProvider {

@Override
public Client newRestEasyClient(Object customJacksonProvider, SSLContext sslContext, boolean disableTrustManager) {
ClientBuilder clientBuilder = ClientBuilderWrapper.create(sslContext, disableTrustManager);
ResteasyClientBuilderImpl builder = createClientBuilder().sslContext(sslContext);

if (customJacksonProvider != null) {
clientBuilder.register(customJacksonProvider, 100);
builder.register(customJacksonProvider, 100);
} else {
clientBuilder.register(JacksonProvider.class, 100);
builder.register(JacksonProvider.class, 100);
}

return clientBuilder.build();
if (disableTrustManager) {
builder.disableTrustManager();
}
return builder.build();
}

/**
* Create a new client builder instance with common defaults and provider registrations
* @return a new client builder
*/
public static ResteasyClientBuilderImpl createClientBuilder() {
return new ResteasyClientBuilderImpl().connectionPoolSize(10).register(StreamMessageBodyReader.class);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.keycloak.admin.client.spi;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.ObjectCodec;
import org.jboss.resteasy.plugins.providers.sse.EventInput;
import org.jboss.resteasy.specimpl.ResponseBuilderImpl;

/**
* Provides a way to read Streams incrementally - valid only for Resteasy Classic
*/
public class StreamMessageBodyReader implements MessageBodyReader<Stream<?>> {

@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type == Stream.class;
}

@Override
public Stream<?> readFrom(Class<Stream<?>> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {

// workaround to obtain an ObjectCodec - it would be better to work directly from a mapper, but
// there's no good assumption to make about how to obtain that
ObjectCodec codec = new ResponseBuilderImpl()
.entity(new ByteArrayInputStream("[]".getBytes(StandardCharsets.UTF_8))).status(200).type(mediaType)
.build().readEntity(JsonParser.class).getCodec();

JsonParser parser = codec.getFactory().createParser(entityStream);
JsonToken token = parser.nextToken();
if (token == null) {
return null;
}
if (token != JsonToken.START_ARRAY) {
entityStream.close();
throw new IOException("Expected Array");
}
token = parser.nextToken();
if (token == JsonToken.END_ARRAY) {
return Stream.of();
}
if (token != JsonToken.START_OBJECT) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work for anything else than streams of objects. Is that correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is the only payload expected with Stream<Object>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I'm thinking if we need the support for streaming primitives as well, but probably not.

entityStream.close();
throw new IOException("Expected Object");
}

Class<?> elementType = Object.class;
@michalvavrik michalvavrik May 25, 2026 •
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are loosing type safety for things IMO should be an illegal state ex (raw, standalone wildcard, type variable and streams of lists). If you want to keep it as is, it is fine with me, but my preference would be to support what we want to support like Stream<ClientRepresentation> and fire exception for other scenarios.

On the other hand, I expect everything to be tested, hence anytime someone would actually use it with our client when the element was Object I'd expect something like ClassCastException downstream, so we will know one way or another.

well, that was waste of words 😀

if (genericType instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
if (typeArguments.length > 0) {
if (typeArguments[0] instanceof WildcardType) {
typeArguments = ((WildcardType)typeArguments[0]).getUpperBounds();
}
if (typeArguments.length > 0 && typeArguments[0] instanceof Class<?>) {
elementType = (Class<?>) typeArguments[0];
}
}
}

Iterator<?> iter = codec.readValues(parser, elementType);
Stream<?> targetStream = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED),
false).onClose(() -> {
try {
entityStream.close();
Comment thread
shawkins marked this conversation as resolved.
Resolved
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
// return a Stream also marked as an EventInput to prevent the premature closure of the result
return (Stream<?>) Proxy.newProxyInstance(EventInput.class.getClassLoader(), new Class<?>[] {Stream.class, EventInput.class}, (proxy, method, args) -> {
try {
return method.invoke(targetStream, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
});
Comment thread
shawkins marked this conversation as resolved.
Resolved
}

}
Loading
0