Repository navigation
Client streaming for admin api v2 #47611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
67d0d60
47389ee
a7691c7
59db65a
5412c56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
| } |
This file was deleted.
| 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) { | ||
| entityStream.close(); | ||
| throw new IOException("Expected Object"); | ||
| } | ||
|
|
||
| Class<?> elementType = Object.class; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 On the other hand, I expect everything to be tested, hence anytime someone would actually use it with our client when the element was 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(); | ||
|
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(); | ||
| } | ||
| }); | ||
|
shawkins marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>There was a problem hiding this comment.
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.