Skip to content

Commit 9d604b8

Browse files
committed
Spring Data Rest: Wrong response schema for collection relations. Fixes #1069
1 parent 1e21f28 commit 9d604b8

File tree

13 files changed

+1391
-151
lines changed

13 files changed

+1391
-151
lines changed

springdoc-openapi-data-rest/src/main/java/org/springdoc/data/rest/SpringRepositoryRestResourceProvider.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public class SpringRepositoryRestResourceProvider implements RepositoryRestResou
8484
/**
8585
* The constant REPOSITORY_ENTITY_CONTROLLER.
8686
*/
87-
public static final String REPOSITORY_ENTITY_CONTROLLER = SPRING_DATA_REST_PACKAGE + ".webmvc.RepositoryEntityController";
87+
private static final String REPOSITORY_ENTITY_CONTROLLER = SPRING_DATA_REST_PACKAGE + ".webmvc.RepositoryEntityController";
8888

8989
/**
9090
* The constant REPOSITORY_SEARCH_CONTROLLER.
@@ -208,6 +208,9 @@ public List<RouterOperation> getRouterOperations(OpenAPI openAPI) {
208208
if (jackson.isExported(property) && associations.isLinkableAssociation(property)) {
209209
dataRestRepository.setRelationName(resourceMetadata.getMappingFor(property).getRel().value());
210210
dataRestRepository.setControllerType(ControllerType.PROPERTY);
211+
dataRestRepository.setCollectionLike(property.isCollectionLike());
212+
dataRestRepository.setMap(property.isMap());
213+
dataRestRepository.setPropertyType(property.getActualType());
211214
findControllers(routerOperationList, handlerMethodMapFilteredMethodMap, resourceMetadata, dataRestRepository, openAPI);
212215
}
213216
});

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ private Operation buildEntityOperation(HandlerMethod handlerMethod, DataRestRepo
155155
domainType = dataRestRepository.getDomainType();
156156
Operation operation = initOperation(handlerMethod, domainType, requestMethod);
157157
dataRestRequestService.buildParameters(domainType, openAPI, handlerMethod, requestMethod, methodAttributes, operation, resourceMetadata);
158-
dataRestResponseService.buildEntityResponse(operation, handlerMethod, openAPI, requestMethod, operationPath, domainType, methodAttributes);
158+
dataRestResponseService.buildEntityResponse(operation, handlerMethod, openAPI, requestMethod, operationPath, domainType, methodAttributes, dataRestRepository);
159159
tagsBuilder.buildEntityTags(operation, handlerMethod, dataRestRepository);
160160
if (domainType != null)
161-
addOperationDescription(operation, requestMethod, domainType.getSimpleName().toLowerCase());
161+
addOperationDescription(operation, requestMethod, domainType.getSimpleName().toLowerCase(), dataRestRepository);
162162
return operation;
163163
}
164164

@@ -241,8 +241,8 @@ private Operation buildSearchOperation(HandlerMethod handlerMethod, DataRestRepo
241241
private Parameter getParameterFromAnnotations(OpenAPI openAPI, MethodAttributes methodAttributes, Method method, String pName) {
242242
Parameter parameter = null;
243243
for (java.lang.reflect.Parameter reflectParameter : method.getParameters()) {
244-
Param paramAnnotation = reflectParameter.getAnnotation(Param.class);
245-
if (paramAnnotation!=null && paramAnnotation.value().equals(pName)) {
244+
Param paramAnnotation = reflectParameter.getAnnotation(Param.class);
245+
if (paramAnnotation != null && paramAnnotation.value().equals(pName)) {
246246
io.swagger.v3.oas.annotations.Parameter parameterDoc = AnnotatedElementUtils.findMergedAnnotation(
247247
AnnotatedElementUtils.forAnnotations(reflectParameter.getAnnotations()),
248248
io.swagger.v3.oas.annotations.Parameter.class);
@@ -277,30 +277,45 @@ private Operation initOperation(HandlerMethod handlerMethod, Class<?> domainType
277277

278278
/**
279279
* Add operation description.
280-
*
281280
* @param operation the operation
282281
* @param requestMethod the request method
283282
* @param entity the entity
283+
* @param dataRestRepository the data rest repository
284284
*/
285-
private void addOperationDescription(Operation operation, RequestMethod requestMethod, String entity) {
285+
private void addOperationDescription(Operation operation, RequestMethod requestMethod, String entity, DataRestRepository dataRestRepository) {
286286
switch (requestMethod) {
287287
case GET:
288-
operation.setDescription("get-" + entity);
288+
operation.setDescription(createDescription("get-", entity, dataRestRepository));
289289
break;
290290
case POST:
291-
operation.setDescription("create-" + entity);
291+
operation.setDescription(createDescription("create-", entity, dataRestRepository));
292292
break;
293293
case DELETE:
294-
operation.setDescription("delete-" + entity);
294+
operation.setDescription(createDescription("delete-", entity, dataRestRepository));
295295
break;
296296
case PUT:
297-
operation.setDescription("update-" + entity);
297+
operation.setDescription(createDescription("update-", entity, dataRestRepository));
298298
break;
299299
case PATCH:
300-
operation.setDescription("patch-" + entity);
300+
operation.setDescription(createDescription("patch-", entity, dataRestRepository));
301301
break;
302302
default:
303303
throw new IllegalArgumentException(requestMethod.name());
304304
}
305305
}
306+
307+
/**
308+
* Create description.
309+
*
310+
* @param entity the entity
311+
* @param dataRestRepository the data rest repository
312+
*/
313+
private String createDescription( String action, String entity, DataRestRepository dataRestRepository) {
314+
String description;
315+
if (dataRestRepository != null && ControllerType.PROPERTY.equals(dataRestRepository.getControllerType()))
316+
description = action + dataRestRepository.getPropertyType().getSimpleName().toLowerCase()+ "-by-"+ entity +"-Id";
317+
else
318+
description = action + entity;
319+
return description;
320+
}
306321
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ public class DataRestRepository {
4949
*/
5050
private ControllerType controllerType;
5151

52+
/**
53+
* The Is collection like.
54+
*/
55+
private boolean isCollectionLike;
56+
57+
/**
58+
* The Is map.
59+
*/
60+
private boolean isMap;
61+
62+
/**
63+
* The Property type.
64+
*/
65+
private Class<?> propertyType;
66+
5267
/**
5368
* Instantiates a new Data rest repository.
5469
*
@@ -131,4 +146,58 @@ public ControllerType getControllerType() {
131146
public void setControllerType(ControllerType controllerType) {
132147
this.controllerType = controllerType;
133148
}
149+
150+
/**
151+
* Is collection like boolean.
152+
*
153+
* @return the boolean
154+
*/
155+
public boolean isCollectionLike() {
156+
return isCollectionLike;
157+
}
158+
159+
/**
160+
* Sets collection like.
161+
*
162+
* @param collectionLike the collection like
163+
*/
164+
public void setCollectionLike(boolean collectionLike) {
165+
isCollectionLike = collectionLike;
166+
}
167+
168+
/**
169+
* Is map boolean.
170+
*
171+
* @return the boolean
172+
*/
173+
public boolean isMap() {
174+
return isMap;
175+
}
176+
177+
/**
178+
* Sets map.
179+
*
180+
* @param map the map
181+
*/
182+
public void setMap(boolean map) {
183+
isMap = map;
184+
}
185+
186+
/**
187+
* Gets property type.
188+
*
189+
* @return the property type
190+
*/
191+
public Class<?> getPropertyType() {
192+
return propertyType;
193+
}
194+
195+
/**
196+
* Sets property type.
197+
*
198+
* @param propertyType the property type
199+
*/
200+
public void setPropertyType(Class<?> propertyType) {
201+
this.propertyType = propertyType;
202+
}
134203
}

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

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import java.lang.reflect.Type;
2828
import java.lang.reflect.WildcardType;
2929
import java.util.Arrays;
30+
import java.util.Map;
3031
import java.util.Objects;
3132
import java.util.Set;
3233

34+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
3335
import io.swagger.v3.oas.models.OpenAPI;
3436
import io.swagger.v3.oas.models.Operation;
3537
import io.swagger.v3.oas.models.media.Content;
@@ -38,13 +40,13 @@
3840
import org.springdoc.core.GenericResponseService;
3941
import org.springdoc.core.MethodAttributes;
4042
import org.springdoc.core.ReturnTypeParser;
41-
import org.springdoc.data.rest.SpringRepositoryRestResourceProvider;
4243

4344
import org.springframework.core.MethodParameter;
4445
import org.springframework.core.ResolvableType;
4546
import org.springframework.data.rest.core.mapping.MethodResourceMapping;
4647
import org.springframework.hateoas.CollectionModel;
4748
import org.springframework.hateoas.EntityModel;
49+
import org.springframework.hateoas.Link;
4850
import org.springframework.hateoas.PagedModel;
4951
import org.springframework.hateoas.RepresentationModel;
5052
import org.springframework.http.HttpEntity;
@@ -123,11 +125,12 @@ public void buildSearchResponse(Operation operation, HandlerMethod handlerMethod
123125
* @param operationPath the operation path
124126
* @param domainType the domain type
125127
* @param methodAttributes the method attributes
128+
* @param dataRestRepository the data rest repository
126129
*/
127130
public void buildEntityResponse(Operation operation, HandlerMethod handlerMethod, OpenAPI openAPI, RequestMethod requestMethod,
128-
String operationPath, Class<?> domainType, MethodAttributes methodAttributes) {
131+
String operationPath, Class<?> domainType, MethodAttributes methodAttributes, DataRestRepository dataRestRepository) {
129132
MethodParameter methodParameterReturn = handlerMethod.getReturnType();
130-
Type returnType = getType(methodParameterReturn, domainType, requestMethod);
133+
Type returnType = getType(methodParameterReturn, domainType, requestMethod, dataRestRepository);
131134
ApiResponses apiResponses = new ApiResponses();
132135
ApiResponse apiResponse = new ApiResponse();
133136
Content content = genericResponseService.buildContent(openAPI.getComponents(), methodParameterReturn.getParameterAnnotations(), methodAttributes.getMethodProduces(), null, returnType);
@@ -204,34 +207,40 @@ else if (ClassUtils.isPrimitiveOrWrapper(methodResourceMapping.getReturnedDomain
204207
* @param methodParameterReturn the method parameter return
205208
* @param domainType the domain type
206209
* @param requestMethod the request method
210+
* @param dataRestRepository the data rest repository
207211
* @return the type
208212
*/
209-
private Type getType(MethodParameter methodParameterReturn, Class<?> domainType, RequestMethod requestMethod) {
213+
private Type getType(MethodParameter methodParameterReturn, Class<?> domainType, RequestMethod requestMethod, DataRestRepository dataRestRepository) {
210214
Type returnType = ReturnTypeParser.resolveType(methodParameterReturn.getGenericParameterType(), methodParameterReturn.getContainingClass());
215+
Class returnedEntityType = domainType;
216+
217+
if (dataRestRepository!=null && ControllerType.PROPERTY.equals(dataRestRepository.getControllerType()))
218+
returnedEntityType = dataRestRepository.getPropertyType();
219+
211220
if (returnType instanceof ParameterizedType) {
212221
ParameterizedType parameterizedType = (ParameterizedType) returnType;
213222
if ((ResponseEntity.class.equals(parameterizedType.getRawType()))) {
214223
if (Object.class.equals(parameterizedType.getActualTypeArguments()[0])) {
215-
return ResolvableType.forClassWithGenerics(ResponseEntity.class, domainType).getType();
224+
return ResolvableType.forClassWithGenerics(ResponseEntity.class, returnedEntityType).getType();
216225
}
217226
else if (parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType) {
218227
ParameterizedType parameterizedType1 = (ParameterizedType) parameterizedType.getActualTypeArguments()[0];
219228
Class<?> rawType = ResolvableType.forType(parameterizedType1.getRawType()).getRawClass();
220229
if (rawType != null && rawType.isAssignableFrom(RepresentationModel.class)) {
221-
Class<?> type = findType(methodParameterReturn, requestMethod);
222-
return resolveGenericType(ResponseEntity.class, type, domainType);
230+
Class<?> type = findType(requestMethod, dataRestRepository);
231+
return resolveGenericType(ResponseEntity.class, type, returnedEntityType);
223232
}
224233
else if (EntityModel.class.equals(parameterizedType1.getRawType())) {
225-
return resolveGenericType(ResponseEntity.class, EntityModel.class, domainType);
234+
return resolveGenericType(ResponseEntity.class, EntityModel.class, returnedEntityType);
226235
}
227236
}
228237
else if (parameterizedType.getActualTypeArguments()[0] instanceof WildcardType) {
229238
WildcardType wildcardType = (WildcardType) parameterizedType.getActualTypeArguments()[0];
230239
if (wildcardType.getUpperBounds()[0] instanceof ParameterizedType) {
231240
ParameterizedType wildcardTypeUpperBound = (ParameterizedType) wildcardType.getUpperBounds()[0];
232241
if (RepresentationModel.class.equals(wildcardTypeUpperBound.getRawType())) {
233-
Class<?> type = findType(methodParameterReturn, requestMethod);
234-
return resolveGenericType(ResponseEntity.class, type, domainType);
242+
Class<?> type = findType(requestMethod, dataRestRepository);
243+
return resolveGenericType(ResponseEntity.class, type, returnedEntityType);
235244
}
236245
}
237246
}
@@ -240,12 +249,12 @@ else if ((HttpEntity.class.equals(parameterizedType.getRawType())
240249
&& parameterizedType.getActualTypeArguments()[0] instanceof ParameterizedType)) {
241250
ParameterizedType wildcardTypeUpperBound = (ParameterizedType) parameterizedType.getActualTypeArguments()[0];
242251
if (RepresentationModel.class.equals(wildcardTypeUpperBound.getRawType())) {
243-
return resolveGenericType(HttpEntity.class, RepresentationModel.class, domainType);
252+
return resolveGenericType(HttpEntity.class, RepresentationModel.class, returnedEntityType);
244253
}
245254
}
246255
else if ((CollectionModel.class.equals(parameterizedType.getRawType())
247256
&& Object.class.equals(parameterizedType.getActualTypeArguments()[0]))) {
248-
return ResolvableType.forClassWithGenerics(CollectionModel.class, domainType).getType();
257+
return ResolvableType.forClassWithGenerics(CollectionModel.class, returnedEntityType).getType();
249258
}
250259
}
251260
return returnType;
@@ -254,14 +263,22 @@ else if ((CollectionModel.class.equals(parameterizedType.getRawType())
254263
/**
255264
* Find type class.
256265
*
257-
* @param methodParameterReturn the method parameter return
258266
* @param requestMethod the request method
267+
* @param dataRestRepository the data rest repository
259268
* @return the class
260269
*/
261-
private Class findType(MethodParameter methodParameterReturn, RequestMethod requestMethod) {
262-
if (SpringRepositoryRestResourceProvider.REPOSITORY_ENTITY_CONTROLLER.equals(methodParameterReturn.getContainingClass().getCanonicalName())
270+
private Class findType(RequestMethod requestMethod, DataRestRepository dataRestRepository) {
271+
if (ControllerType.ENTITY.equals(dataRestRepository.getControllerType())
263272
&& Arrays.stream(requestMethodsEntityModel).anyMatch(requestMethod::equals))
264273
return EntityModel.class;
274+
else if (dataRestRepository!=null && ControllerType.PROPERTY.equals(dataRestRepository.getControllerType())) {
275+
if (dataRestRepository.isCollectionLike())
276+
return CollectionModel.class;
277+
else if (dataRestRepository.isMap())
278+
return MapModel.class;
279+
else
280+
return EntityModel.class;
281+
}
265282
else
266283
return RepresentationModel.class;
267284
}
@@ -306,4 +323,37 @@ private void addResponse204(ApiResponses apiResponses) {
306323
private void addResponse404(ApiResponses apiResponses) {
307324
apiResponses.put(String.valueOf(HttpStatus.NOT_FOUND.value()), new ApiResponse().description(HttpStatus.NOT_FOUND.getReasonPhrase()));
308325
}
326+
327+
/**
328+
* The type Map model.
329+
* @author bnasslashen
330+
*/
331+
private static class MapModel extends RepresentationModel<MapModel> {
332+
/**
333+
* The Content.
334+
*/
335+
private Map<? extends Object, ? extends Object> content;
336+
337+
/**
338+
* Instantiates a new Map model.
339+
*
340+
* @param content the content
341+
* @param links the links
342+
*/
343+
public MapModel(Map<? extends Object, ? extends Object> content, Link... links) {
344+
super(Arrays.asList(links));
345+
this.content = content;
346+
}
347+
348+
/**
349+
* Gets content.
350+
*
351+
* @return the content
352+
*/
353+
@JsonAnyGetter
354+
public Map<? extends Object, ? extends Object> getContent() {
355+
return content;
356+
}
357+
}
358+
309359
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package test.org.springdoc.api.app23;
2+
3+
import javax.persistence.Entity;
4+
import javax.persistence.GeneratedValue;
5+
import javax.persistence.GenerationType;
6+
import javax.persistence.Id;
7+
import javax.validation.constraints.NotBlank;
8+
import javax.validation.constraints.NotNull;
9+
10+
import lombok.AllArgsConstructor;
11+
import lombok.Builder;
12+
import lombok.Data;
13+
import lombok.EqualsAndHashCode;
14+
import lombok.NoArgsConstructor;
15+
16+
@Data
17+
@Entity
18+
@NoArgsConstructor
19+
@AllArgsConstructor
20+
@EqualsAndHashCode(callSuper = false)
21+
@Builder
22+
public class Clinic {
23+
24+
@Id
25+
@GeneratedValue(strategy = GenerationType.AUTO) private Long id;
26+
27+
@NotNull
28+
@NotBlank
29+
private String name;
30+
31+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package test.org.springdoc.api.app23;
2+
3+
import org.springframework.data.repository.CrudRepository;
4+
import org.springframework.web.bind.annotation.CrossOrigin;
5+
6+
@CrossOrigin
7+
public interface ClinicRepo extends CrudRepository<Clinic, Long> {
8+
}

0 commit comments

Comments
 (0)