Skip to content

Commit f380db5

Browse files
committed
implemented qdsl predicate annotation support using operation customiser
1 parent f2e74af commit f380db5

File tree

9 files changed

+471
-2
lines changed

9 files changed

+471
-2
lines changed

springdoc-openapi-common/src/main/java/org/springdoc/core/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public final class Constants {
4646

4747
public static final String SPRINGDOC_SWAGGER_UI_URL = "springdoc.swagger-ui.url";
4848

49+
public static final String SPRINGDOC_QDSLPREDICATE_MODE = "springdoc.qdslpredicate.mode";
50+
4951
public static final String NULL = ":#{null}";
5052

5153
public static final String MVC_SERVLET_PATH = "${spring.mvc.servlet.path"+ NULL +"}";

springdoc-openapi-data-rest/src/main/java/org/springdoc/core/SpringDocDataRestConfiguration.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,23 @@
3232
import org.springdoc.core.converters.Pageable;
3333
import org.springdoc.core.converters.QueryDslPredicateConverter;
3434
import org.springdoc.core.converters.RepresentationModelLinksOASMixin;
35+
import org.springdoc.core.customisers.QuerydslPredicateOperationCustomizer;
3536
import org.springdoc.core.customizers.OpenApiCustomiser;
3637

3738
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3839
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3940
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
4041
import org.springframework.context.annotation.Bean;
4142
import org.springframework.context.annotation.Configuration;
43+
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
4244
import org.springframework.data.querydsl.binding.QuerydslBindingsFactory;
4345
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
4446
import org.springframework.hateoas.Link;
4547
import org.springframework.hateoas.Links;
4648
import org.springframework.hateoas.RepresentationModel;
4749

4850
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
51+
import static org.springdoc.core.Constants.SPRINGDOC_QDSLPREDICATE_MODE;
4952
import static org.springdoc.core.SpringDocUtils.getConfig;
5053

5154
@Configuration
@@ -59,9 +62,18 @@ public class SpringDocDataRestConfiguration {
5962

6063
@Bean
6164
@ConditionalOnMissingBean
62-
@ConditionalOnClass(QuerydslBindingsFactory.class)
65+
@ConditionalOnProperty(name = SPRINGDOC_QDSLPREDICATE_MODE, havingValue = "object")
66+
@ConditionalOnClass(QuerydslBindingsFactory.class)
6367
public QueryDslPredicateConverter qdslConverter(QuerydslBindingsFactory querydslBindingsFactory) {
64-
return new QueryDslPredicateConverter(querydslBindingsFactory);
68+
return new QueryDslPredicateConverter(querydslBindingsFactory);
69+
}
70+
71+
@Bean
72+
@ConditionalOnMissingBean({QuerydslPredicateOperationCustomizer.class, QueryDslPredicateConverter.class})
73+
@ConditionalOnClass(QuerydslBindingsFactory.class)
74+
public QuerydslPredicateOperationCustomizer querydslPredicateOperationCustomizer(QuerydslBindingsFactory querydslBindingsFactory,
75+
LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer) {
76+
return new QuerydslPredicateOperationCustomizer(querydslBindingsFactory, localVariableTableParameterNameDiscoverer);
6577
}
6678

6779
@Configuration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package org.springdoc.core.customisers;
2+
3+
import com.querydsl.core.types.Path;
4+
import io.swagger.v3.core.converter.ModelConverters;
5+
import io.swagger.v3.core.converter.ResolvedSchema;
6+
import io.swagger.v3.core.util.PrimitiveType;
7+
import io.swagger.v3.oas.models.Operation;
8+
import io.swagger.v3.oas.models.media.Schema;
9+
import io.swagger.v3.oas.models.parameters.Parameter;
10+
import org.apache.commons.lang3.StringUtils;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
import org.springdoc.core.customizers.OperationCustomizer;
14+
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
15+
import org.springframework.core.MethodParameter;
16+
import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
17+
import org.springframework.data.querydsl.binding.QuerydslBindings;
18+
import org.springframework.data.querydsl.binding.QuerydslBindingsFactory;
19+
import org.springframework.data.querydsl.binding.QuerydslPredicate;
20+
import org.springframework.data.util.CastUtils;
21+
import org.springframework.data.util.ClassTypeInformation;
22+
import org.springframework.data.util.TypeInformation;
23+
import org.springframework.web.method.HandlerMethod;
24+
25+
import java.lang.reflect.Field;
26+
import java.lang.reflect.Modifier;
27+
import java.lang.reflect.Type;
28+
import java.util.*;
29+
import java.util.stream.Collectors;
30+
31+
/**
32+
* @author Gibah Joseph
33+
34+
* Mar, 2020
35+
**/
36+
public class QuerydslPredicateOperationCustomizer implements OperationCustomizer {
37+
private static final Logger LOGGER = LoggerFactory.getLogger(QuerydslPredicateOperationCustomizer.class);
38+
private QuerydslBindingsFactory querydslBindingsFactory;
39+
private LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer;
40+
41+
public QuerydslPredicateOperationCustomizer(QuerydslBindingsFactory querydslBindingsFactory, LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer) {
42+
this.querydslBindingsFactory = querydslBindingsFactory;
43+
this.localVariableTableParameterNameDiscoverer = localVariableTableParameterNameDiscoverer;
44+
}
45+
46+
@Override
47+
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
48+
if (operation.getParameters() == null) {
49+
return operation;
50+
}
51+
52+
MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
53+
String[] methodParameterNames = this.localVariableTableParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
54+
String[] reflectionParametersNames = Arrays.stream(methodParameters).map(MethodParameter::getParameterName).toArray(String[]::new);
55+
if (methodParameterNames == null) {
56+
methodParameterNames = reflectionParametersNames;
57+
}
58+
int parametersLength = methodParameters.length;
59+
List<Parameter> parametersToAddToOperation = new ArrayList<>();
60+
for (int i = 0; i < parametersLength; i++) {
61+
MethodParameter parameter = methodParameters[i];
62+
QuerydslPredicate predicate = parameter.getParameterAnnotation(QuerydslPredicate.class);
63+
64+
if (predicate == null) {
65+
continue;
66+
}
67+
68+
List<io.swagger.v3.oas.models.parameters.Parameter> operationParameters = operation.getParameters();
69+
70+
Parameter predicateParam = getParameterFromOperationByName(operationParameters, methodParameterNames[i]);
71+
// remove @QueryDslPredicate param from operation to avoid duplicates
72+
if (predicateParam != null) {
73+
operationParameters.remove(predicateParam);
74+
}
75+
76+
QuerydslBindings bindings = extractQdslBindings(predicate);
77+
78+
Set<String> fieldsToAdd = Arrays.stream(predicate.root().getDeclaredFields()).map(Field::getName).collect(Collectors.toSet());
79+
80+
Map<String, Object> pathSpecMap = getPathSpec(bindings, "pathSpecs");
81+
//remove blacklisted fields
82+
Set<String> blacklist = getFieldValues(bindings, "blackList");
83+
fieldsToAdd.removeIf(blacklist::contains);
84+
85+
Set<String> whiteList = getFieldValues(bindings, "whiteList");
86+
Set<String> aliases = getFieldValues(bindings, "aliases");
87+
88+
fieldsToAdd.addAll(aliases);
89+
fieldsToAdd.addAll(whiteList);
90+
for (String fieldName : fieldsToAdd) {
91+
Type type = getFieldType(fieldName, pathSpecMap, predicate.root());
92+
io.swagger.v3.oas.models.parameters.Parameter newParameter = buildParam(type, fieldName);
93+
94+
parametersToAddToOperation.add(newParameter);
95+
}
96+
}
97+
operation.getParameters().addAll(parametersToAddToOperation);
98+
return operation;
99+
}
100+
101+
private Parameter getParameterFromOperationByName(List<Parameter> operationParameters, String parameterName) {
102+
return operationParameters.stream()
103+
.filter(parameter1 -> parameter1.getName().equals(parameterName))
104+
.findFirst().orElse(null);
105+
}
106+
107+
private QuerydslBindings extractQdslBindings(QuerydslPredicate predicate) {
108+
ClassTypeInformation<?> classTypeInformation = ClassTypeInformation.from(predicate.root());
109+
TypeInformation<?> domainType = classTypeInformation.getRequiredActualType();
110+
111+
Optional<Class<? extends QuerydslBinderCustomizer<?>>> bindingsAnnotation = Optional.of(predicate)
112+
.map(QuerydslPredicate::bindings)
113+
.map(CastUtils::cast);
114+
115+
return bindingsAnnotation
116+
.map(it -> querydslBindingsFactory.createBindingsFor(domainType, it))
117+
.orElseGet(() -> querydslBindingsFactory.createBindingsFor(domainType));
118+
}
119+
120+
private Set<String> getFieldValues(QuerydslBindings instance, String fieldName) {
121+
try {
122+
Field field = instance.getClass().getDeclaredField(fieldName);
123+
if (Modifier.isPrivate(field.getModifiers())) {
124+
field.setAccessible(true);
125+
}
126+
return (Set<String>) field.get(instance);
127+
} catch (NoSuchFieldException | IllegalAccessException e) {
128+
LOGGER.warn("NoSuchFieldException or IllegalAccessException occurred : {}", e.getMessage());
129+
}
130+
return Collections.emptySet();
131+
}
132+
133+
private Map<String, Object> getPathSpec(QuerydslBindings instance, String fieldName) {
134+
try {
135+
Field field = instance.getClass().getDeclaredField(fieldName);
136+
if (Modifier.isPrivate(field.getModifiers())) {
137+
field.setAccessible(true);
138+
}
139+
return (Map<String, Object>) field.get(instance);
140+
} catch (NoSuchFieldException | IllegalAccessException e) {
141+
LOGGER.warn("NoSuchFieldException or IllegalAccessException occurred : {}", e.getMessage());
142+
}
143+
return Collections.emptyMap();
144+
}
145+
146+
private Optional<Path<?>> getPathFromPathSpec(Object instance) {
147+
try {
148+
if (instance == null) {
149+
return Optional.empty();
150+
}
151+
Field field = instance.getClass().getDeclaredField("path");
152+
if (Modifier.isPrivate(field.getModifiers())) {
153+
field.setAccessible(true);
154+
}
155+
return (Optional<Path<?>>) field.get(instance);
156+
} catch (NoSuchFieldException | IllegalAccessException e) {
157+
LOGGER.warn("NoSuchFieldException or IllegalAccessException occurred : {}", e.getMessage());
158+
}
159+
return Optional.empty();
160+
}
161+
162+
/***
163+
* Tries to figure out the Type of the field. It first checks the Qdsl pathSpecMap before checking the root class. Defaults to String.class
164+
* @param fieldName The name of the field used as reference to get the type
165+
* @param pathSpecMap The Qdsl path specifications as defined in the resolved bindings
166+
* @param root The root type where the paths are gotten
167+
* @return The type of the field. Returns
168+
*/
169+
private Type getFieldType(String fieldName, Map<String, Object> pathSpecMap, Class<?> root) {
170+
try {
171+
Object pathAndBinding = pathSpecMap.get(fieldName);
172+
Optional<Path<?>> path = getPathFromPathSpec(pathAndBinding);
173+
174+
Type genericType;
175+
Field declaredField = null;
176+
if (path.isPresent()) {
177+
genericType = path.get().getType();
178+
} else {
179+
declaredField = root.getDeclaredField(fieldName);
180+
genericType = declaredField.getGenericType();
181+
}
182+
if (genericType != null) {
183+
return genericType;
184+
}
185+
} catch (NoSuchFieldException e) {
186+
LOGGER.warn("Field {} not found on {} : {}", fieldName, root.getName(), e.getMessage());
187+
}
188+
return String.class;
189+
}
190+
191+
/***
192+
* Constructs the parameter
193+
* @param type The type of the parameter
194+
* @param name The name of the parameter
195+
* @return The swagger parameter
196+
*/
197+
private io.swagger.v3.oas.models.parameters.Parameter buildParam(Type type, String name) {
198+
io.swagger.v3.oas.models.parameters.Parameter parameter = new io.swagger.v3.oas.models.parameters.Parameter();
199+
200+
if (StringUtils.isBlank(parameter.getName())) {
201+
parameter.setName(name);
202+
}
203+
204+
if (StringUtils.isBlank(parameter.getIn())) {
205+
parameter.setIn("query");
206+
}
207+
parameter.setRequired(false);
208+
209+
if (parameter.getSchema() == null) {
210+
Schema<?> schema = null;
211+
PrimitiveType primitiveType = PrimitiveType.fromType(type);
212+
if (primitiveType != null) {
213+
schema = primitiveType.createProperty();
214+
} else {
215+
ResolvedSchema resolvedSchema = ModelConverters.getInstance()
216+
.resolveAsResolvedSchema(
217+
new io.swagger.v3.core.converter.AnnotatedType(type).resolveAsRef(true));
218+
// could not resolve the schema or this schema references other schema
219+
// we dont want this since there's no reference to the components in order to register a new schema if it doesnt already exist
220+
// defaulting to string
221+
if (resolvedSchema == null || !resolvedSchema.referencedSchemas.isEmpty()) {
222+
schema = PrimitiveType.fromType(String.class).createProperty();
223+
} else {
224+
schema = resolvedSchema.schema;
225+
}
226+
}
227+
parameter.setSchema(schema);
228+
}
229+
return parameter;
230+
}
231+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package test.org.springdoc.api.app100;
2+
3+
/**
4+
* @author Gibah Joseph
5+
6+
* Mar, 2020
7+
**/
8+
public class ChildEntity {
9+
private Long id;
10+
11+
public Long getId() {
12+
return id;
13+
}
14+
15+
public ChildEntity setId(Long id) {
16+
this.id = id;
17+
return this;
18+
}
19+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package test.org.springdoc.api.app100;
2+
3+
import java.util.List;
4+
5+
/**
6+
* @author Gibah Joseph
7+
8+
* Mar, 2020
9+
**/
10+
public class DummyEntity {
11+
private String name;
12+
private String code;
13+
private SpringDocApp100Test.Status status;
14+
private ChildEntity child;
15+
private List<SpringDocApp100Test.Status> notStatuses;
16+
17+
public List<SpringDocApp100Test.Status> getNotStatuses() {
18+
return notStatuses;
19+
}
20+
21+
public DummyEntity setNotStatuses(List<SpringDocApp100Test.Status> notStatuses) {
22+
this.notStatuses = notStatuses;
23+
return this;
24+
}
25+
26+
public SpringDocApp100Test.Status getStatus() {
27+
return status;
28+
}
29+
30+
public DummyEntity setStatus(SpringDocApp100Test.Status status) {
31+
this.status = status;
32+
return this;
33+
}
34+
35+
public ChildEntity getChild() {
36+
return child;
37+
}
38+
39+
public DummyEntity setChild(ChildEntity child) {
40+
this.child = child;
41+
return this;
42+
}
43+
44+
public String getName() {
45+
return name;
46+
}
47+
48+
public void setName(String name) {
49+
this.name = name;
50+
}
51+
52+
public String getCode() {
53+
return code;
54+
}
55+
56+
public void setCode(String code) {
57+
this.code = code;
58+
}
59+
60+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package test.org.springdoc.api.app100;
2+
3+
import com.querydsl.core.types.Path;
4+
import com.querydsl.core.types.PathMetadata;
5+
import com.querydsl.core.types.dsl.EntityPathBase;
6+
import com.querydsl.core.types.dsl.NumberPath;
7+
8+
import static com.querydsl.core.types.PathMetadataFactory.forVariable;
9+
10+
/**
11+
* @author Gibah Joseph
12+
13+
* Mar, 2020
14+
**/
15+
public class QChildEntity extends EntityPathBase<ChildEntity> {
16+
17+
public static final QChildEntity childEntity = new QChildEntity("childEntity");
18+
private static final long serialVersionUID = -1184258693L;
19+
public final NumberPath<Long> id = createNumber("id", Long.class);
20+
21+
public QChildEntity(String variable) {
22+
super(ChildEntity.class, forVariable(variable));
23+
}
24+
25+
public QChildEntity(Path<? extends ChildEntity> path) {
26+
super(path.getType(), path.getMetadata());
27+
}
28+
29+
public QChildEntity(PathMetadata metadata) {
30+
super(ChildEntity.class, metadata);
31+
}
32+
}

0 commit comments

Comments
 (0)