Skip to content

Commit

Permalink
Represent Java BaseStream<T, S> as array of T in generated schema
Browse files Browse the repository at this point in the history
Fixes smallrye#1359

Signed-off-by: Michael Edgar <[email protected]>
  • Loading branch information
MikeEdgar committed Mar 7, 2023
1 parent 1f053cf commit b1e23c6
Show file tree
Hide file tree
Showing 21 changed files with 388 additions and 99 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.BaseStream;

import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
Expand Down Expand Up @@ -67,14 +69,14 @@
*/
public class OpenApiDataObjectScanner {

// Collection (list-type things)
public static final DotName COLLECTION_INTERFACE_NAME = DotName.createSimple(Collection.class.getName());
public static final Type COLLECTION_TYPE = Type.create(COLLECTION_INTERFACE_NAME, Type.Kind.CLASS);

// Iterable (also list-type things)
public static final DotName ITERABLE_INTERFACE_NAME = DotName.createSimple(Iterable.class.getName());
public static final Type ITERABLE_TYPE = Type.create(ITERABLE_INTERFACE_NAME, Type.Kind.CLASS);

// Stream
public static final DotName STREAM_INTERFACE_NAME = DotName.createSimple(BaseStream.class.getName());
public static final Type STREAM_TYPE = Type.create(STREAM_INTERFACE_NAME, Type.Kind.CLASS);

// Map
public static final DotName MAP_INTERFACE_NAME = DotName.createSimple(Map.class.getName());
public static final Type MAP_TYPE = Type.create(MAP_INTERFACE_NAME, Type.Kind.CLASS);
Expand All @@ -93,23 +95,25 @@ public class OpenApiDataObjectScanner {
// Array type
public static final Type ARRAY_TYPE_OBJECT = Type.create(DotName.createSimple(Map[].class.getName()), Type.Kind.ARRAY);

private static ClassInfo collectionStandin;
private static ClassInfo iterableStandin;
private static ClassInfo mapStandin;
private static ClassInfo streamStandin;
private static List<ClassInfo> standinClasses;

/*-
* Index the "standin" collection types for internal use. These are required to wrap
* collections of application classes (indexed elsewhere).
*/
static {
Indexer indexer = new Indexer();
index(indexer, "CollectionStandin.class");
index(indexer, "IterableStandin.class");
index(indexer, "MapStandin.class");
index(indexer, "StreamStandin.class");
Index index = indexer.complete();
collectionStandin = index.getClassByName(DotName.createSimple(CollectionStandin.class.getName()));
iterableStandin = index.getClassByName(DotName.createSimple(IterableStandin.class.getName()));
mapStandin = index.getClassByName(DotName.createSimple(MapStandin.class.getName()));
streamStandin = index.getClassByName(DotName.createSimple(StreamStandin.class.getName()));
standinClasses = Arrays.asList(iterableStandin, mapStandin, streamStandin);
}

private static void index(Indexer indexer, String resourceName) {
Expand Down Expand Up @@ -208,8 +212,8 @@ Schema process() {
DataObjectDeque.PathEntry root = objectStack.rootNode(rootAnnotationTarget, rootClassInfo, rootClassType, rootSchema);

// For certain special types (map, list, etc) we need to do some pre-processing.
if (isSpecialType(rootClassType)) {
resolveSpecial(root, rootClassType);
if (standinClasses.contains(rootClassInfo)) {
resolveSpecial(root, rootClassType, rootClassInfo); // NOSONAR - rootClassInfo is known to be non-null
} else {
objectStack.push(root);
}
Expand Down Expand Up @@ -322,29 +326,30 @@ private Schema readKlass(ClassInfo currentClass,
return classSchema;
}

private void resolveSpecial(DataObjectDeque.PathEntry root, Type type) {
if (typeArgumentMismatch(type, rootClassInfo)) {
private void resolveSpecial(DataObjectDeque.PathEntry root, Type type, ClassInfo standin) {
ParameterizedType standinType = standin.interfaceTypes().get(0).asParameterizedType();

if (typeArgumentMismatch(type, standin)) {
/*
* The type's generic arguments are not in alignment with the type
* parameters of the stand-in collection type. For the purposes
* of obtaining the stand-in collection's schema, we discard the
* original type and determine the correct type parameter for the
* stand-in.
*/
ParameterizedType standinInterface = rootClassInfo.interfaceTypes().get(0).asParameterizedType();
type = TypeResolver.resolveParameterizedAncestor(context,
type.asParameterizedType(),
standinInterface);
type = TypeResolver.resolveParameterizedAncestor(context, type, standinType)
.map(Type.class::cast)
.orElse(type);
}

Map<String, TypeResolver> fieldResolution = TypeResolver.getAllFields(context, type, rootClassInfo,
Map<String, TypeResolver> fieldResolution = TypeResolver.getAllFields(context, type, standin,
root.getAnnotationTarget());
rootSchema = preProcessSpecial(type, fieldResolution.values().iterator().next(), root);
}

private boolean typeArgumentMismatch(Type type, ClassInfo standin) {
if (type.kind() != Kind.PARAMETERIZED_TYPE) {
return false;
return true;
}

return standin.typeParameters().size() < type.asParameterizedType().arguments().size();
Expand All @@ -358,16 +363,7 @@ private boolean isA(Type testSubject, Type test) {
return TypeUtil.isA(context, testSubject, test);
}

// Is Map, Collection, etc.
private boolean isSpecialType(Type type) {
return isA(type, COLLECTION_TYPE) || isA(type, ITERABLE_TYPE) || isA(type, MAP_TYPE);
}

private ClassInfo initialType(Type type) {
if (isA(type, COLLECTION_TYPE)) {
return collectionStandin;
}

if (isA(type, ITERABLE_TYPE)) {
return iterableStandin;
}
Expand All @@ -376,6 +372,10 @@ private ClassInfo initialType(Type type) {
return mapStandin;
}

if (isA(type, STREAM_TYPE)) {
return streamStandin;
}

return index.getClass(type);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.smallrye.openapi.runtime.scanner;

import java.util.stream.BaseStream;

public abstract class StreamStandin<E, S> implements BaseStream<E, StreamStandin<E, S>> {
E value;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package io.smallrye.openapi.runtime.scanner.dataobject;

import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.ARRAY_TYPE_OBJECT;
import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.COLLECTION_TYPE;
import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.ENUM_TYPE;
import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.ITERABLE_TYPE;
import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.MAP_TYPE;
import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.SET_TYPE;
import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.STREAM_TYPE;
import static io.smallrye.openapi.runtime.scanner.OpenApiDataObjectScanner.STRING_TYPE;
import static io.smallrye.openapi.runtime.util.TypeUtil.isTerminalType;

Expand Down Expand Up @@ -115,19 +115,25 @@ public Type processType() {
return readParameterizedType(type.asParameterizedType(), this.schema);
}

// Raw Collection
if (isA(type, COLLECTION_TYPE)) {
return ARRAY_TYPE_OBJECT;
}

// Raw Iterable
if (isA(type, ITERABLE_TYPE)) {
return ARRAY_TYPE_OBJECT;
return TypeResolver.resolveParameterizedAncestor(context, type, ITERABLE_TYPE)
.map(p -> readParameterizedType(p.asParameterizedType(), this.schema))
.orElse(ARRAY_TYPE_OBJECT);
}

// Raw Stream
if (isA(type, STREAM_TYPE)) {
return TypeResolver.resolveParameterizedAncestor(context, type, STREAM_TYPE)
.map(p -> readParameterizedType(p.asParameterizedType(), this.schema))
.orElse(ARRAY_TYPE_OBJECT);
}

// Raw Map
if (isA(type, MAP_TYPE)) {
return MAP_TYPE;
return TypeResolver.resolveParameterizedAncestor(context, type, MAP_TYPE)
.map(p -> readParameterizedType(p.asParameterizedType(), this.schema))
.orElse(MAP_TYPE);
}

// Simple case: bare class or primitive type.
Expand Down Expand Up @@ -187,18 +193,20 @@ private Type readArrayType(ArrayType arrayType, Schema arraySchema) {
private Type readParameterizedType(ParameterizedType pType, Schema schema) {
DataObjectLogging.logger.processingParametrizedType(pType);
Type typeRead = pType;
Type seekType = resolveSeekType(pType);

// If it's a collection, we should treat it as an array.
if (isA(pType, COLLECTION_TYPE) || isA(pType, ITERABLE_TYPE)) {
DataObjectLogging.logger.processingTypeAs("Java Collection", "Array");
// If it's a collection, iterable, or a stream, we should treat it as an array.
if (seekType != null && seekType != MAP_TYPE) {
DataObjectLogging.logger.processingTypeAs("Java Iterable or Stream", "Array");
schema.type(Schema.SchemaType.ARRAY);
ParameterizedType ancestorType = TypeResolver.resolveParameterizedAncestor(context, pType, ITERABLE_TYPE);
ParameterizedType ancestorType = TypeResolver.resolveParameterizedAncestor(context, pType, seekType)
.orElse(pType);

if (TypeUtil.isA(context, pType, SET_TYPE)) {
schema.setUniqueItems(Boolean.TRUE);
}

// Should only have one arg for collection.
// Should only have one argument for Iterable and Stream uses first argument of BaseStream.
Type valueType = ancestorType.arguments().get(0);
boolean isOptional = TypeUtil.isOptional(valueType);
if (isOptional) {
Expand All @@ -213,10 +221,11 @@ private Type readParameterizedType(ParameterizedType pType, Schema schema) {
schema.setItems(valueSchema);

typeRead = ARRAY_TYPE_OBJECT; // Representing collection as JSON array
} else if (isA(pType, MAP_TYPE)) {
} else if (seekType == MAP_TYPE) {
DataObjectLogging.logger.processingTypeAs("Map", "object");
schema.type(Schema.SchemaType.OBJECT);
ParameterizedType ancestorType = TypeResolver.resolveParameterizedAncestor(context, pType, MAP_TYPE);
ParameterizedType ancestorType = TypeResolver.resolveParameterizedAncestor(context, pType, seekType)
.orElse(pType);

if (ancestorType.arguments().size() == 2) {
Type valueType = ancestorType.arguments().get(1);
Expand All @@ -238,6 +247,19 @@ private Type readParameterizedType(ParameterizedType pType, Schema schema) {
return typeRead;
}

private Type resolveSeekType(ParameterizedType pType) {
if (isA(pType, ITERABLE_TYPE)) {
return ITERABLE_TYPE;
}
if (isA(pType, MAP_TYPE)) {
return MAP_TYPE;
}
if (isA(pType, STREAM_TYPE)) {
return STREAM_TYPE;
}
return null;
}

private static Schema wrapOptionalItemSchema(Schema itemSchema) {
return new SchemaImpl()
.nullable(Boolean.TRUE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
Expand All @@ -26,8 +27,10 @@
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.CompositeIndex;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.Type;
Expand Down Expand Up @@ -455,17 +458,15 @@ public static TypeResolver forClass(AnnotationScannerContext context, ClassInfo
Type currentType = entry.getValue();

if (currentType.kind() == Type.Kind.PARAMETERIZED_TYPE) {
Map<String, Type> resMap = buildParamTypeResolutionMap(currentClass, currentType.asParameterizedType());
stack.push(resMap);
stack.push(buildParamTypeResolutionMap(currentClass, currentType));
}

// Add parameter type information from any interfaces implemented by this class/interface
JandexUtil.interfaces(index, currentClass)
.stream()
.filter(type -> type.kind() == Type.Kind.PARAMETERIZED_TYPE)
.filter(type -> !TypeUtil.knownJavaType(type.name()))
.filter(index::containsClass)
.map(type -> buildParamTypeResolutionMap(index.getClass(type), type.asParameterizedType()))
.map(type -> buildParamTypeResolutionMap(index.getClass(type), type))
.forEach(stack::push);

if (allOfMatch || (!currentType.equals(clazzType) && TypeUtil.isIncludedAllOf(clazz, currentType))) {
Expand All @@ -490,8 +491,7 @@ public static Map<String, TypeResolver> getAllFields(AnnotationScannerContext co
Type currentType = entry.getValue();

if (currentType.kind() == Type.Kind.PARAMETERIZED_TYPE) {
Map<String, Type> resMap = buildParamTypeResolutionMap(currentClass, currentType.asParameterizedType());
stack.push(resMap);
stack.push(buildParamTypeResolutionMap(currentClass, currentType));
}

if (skipPropertyScan || (!currentType.equals(leaf) && TypeUtil.isIncludedAllOf(leafKlazz, currentType))
Expand Down Expand Up @@ -1105,13 +1105,22 @@ private static Map<String, Type> buildParamTypeResolutionMap(ClassInfo klazz, Pa
return resolutionMap;
}

public static ParameterizedType resolveParameterizedAncestor(AnnotationScannerContext context, ParameterizedType pType,
private static Map<String, Type> buildParamTypeResolutionMap(ClassInfo klazz, Type type) {
if (type.kind() == Type.Kind.PARAMETERIZED_TYPE) {
return buildParamTypeResolutionMap(klazz, type.asParameterizedType());
}
return Collections.emptyMap();
}

public static Optional<ParameterizedType> resolveParameterizedAncestor(AnnotationScannerContext context,
Type type,
Type seekType) {
ParameterizedType cursor = pType;
IndexView index = CompositeIndex.create(context.getAugmentedIndex(), TypeUtil.jdkIndex);
Type cursor = type;
boolean seekContinue = true;
ClassInfo cursorClass;

while (context.getAugmentedIndex().containsClass(cursor) && seekContinue) {
ClassInfo cursorClass = context.getIndex().getClassByName(cursor.name());
while ((cursorClass = index.getClassByName(cursor.name())) != null && seekContinue) {
Map<String, Type> resolutionMap = buildParamTypeResolutionMap(cursorClass, cursor);
List<Type> interfaces = getInterfacesOfType(context, cursorClass, seekType);

Expand All @@ -1137,7 +1146,7 @@ public static ParameterizedType resolveParameterizedAncestor(AnnotationScannerCo
}
}

return cursor;
return cursor.kind() == Type.Kind.PARAMETERIZED_TYPE ? Optional.of(cursor.asParameterizedType()) : Optional.empty();
}

private static List<Type> getInterfacesOfType(AnnotationScannerContext context, ClassInfo clazz, Type seekType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1481,7 +1481,7 @@ static MethodInfo targetMethod(AnnotationTarget target) {
*/
protected void readParametersInherited(ClassInfo clazz, AnnotationInstance beanParamAnnotation,
boolean overriddenParametersOnly) {
AugmentedIndexView augmentedIndex = AugmentedIndexView.augment(index);
AugmentedIndexView augmentedIndex = scannerContext.getAugmentedIndex();
List<ClassInfo> ancestors = new ArrayList<>(JandexUtil.inheritanceChain(index, clazz, null).keySet());
/*
* Process parent class(es) before the resource method class to allow for overridden parameter attributes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public class TypeUtil {
.example("13:45.30.123456789").build();

private static final Map<DotName, TypeWithFormat> TYPE_MAP = new LinkedHashMap<>();
private static final IndexView jdkIndex;
public static final IndexView jdkIndex;
private static final Set<DotName> wrapperTypes = new HashSet<>();

// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#dataTypeFormat
Expand Down Expand Up @@ -264,6 +264,13 @@ public class TypeUtil {
index(indexer, java.util.concurrent.PriorityBlockingQueue.class);
index(indexer, java.util.concurrent.SynchronousQueue.class);

// Streams
index(indexer, java.util.stream.BaseStream.class);
index(indexer, java.util.stream.Stream.class);
index(indexer, java.util.stream.IntStream.class);
index(indexer, java.util.stream.LongStream.class);
index(indexer, java.util.stream.DoubleStream.class);

// CompletionStage and implementation
index(indexer, java.util.concurrent.CompletionStage.class);
index(indexer, java.util.concurrent.CompletableFuture.class);
Expand Down
Loading

0 comments on commit b1e23c6

Please sign in to comment.