Skip to content

Commit 301f96d

Browse files
committed
Support for @operation in @RepositoryRestResource Spring Data Repositories. Fixes #1026.
1 parent 2d61892 commit 301f96d

File tree

10 files changed

+865
-39
lines changed

10 files changed

+865
-39
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ private void buildApiResponses(Components components, MethodParameter methodPara
318318
* @param method the method
319319
* @return the api responses
320320
*/
321-
private Set<io.swagger.v3.oas.annotations.responses.ApiResponse> getApiResponses(Method method) {
321+
public Set<io.swagger.v3.oas.annotations.responses.ApiResponse> getApiResponses(Method method) {
322322
Class<?> declaringClass = method.getDeclaringClass();
323323

324324
Set<io.swagger.v3.oas.annotations.responses.ApiResponses> apiResponsesDoc = AnnotatedElementUtils

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springdoc.core.GenericParameterService;
3232
import org.springdoc.core.GenericResponseService;
3333
import org.springdoc.core.OpenAPIService;
34+
import org.springdoc.core.OperationService;
3435
import org.springdoc.core.RequestBodyService;
3536
import org.springdoc.core.SpringDocConfigProperties;
3637
import org.springdoc.core.converters.models.DefaultPageable;
@@ -199,13 +200,14 @@ DataRestRouterOperationService dataRestRouterOperationBuilder(DataRestOperationS
199200
* @param dataRestRequestService the data rest request builder
200201
* @param tagsBuilder the tags builder
201202
* @param dataRestResponseService the data rest response builder
203+
* @param operationService the operation service
202204
* @return the data rest operation builder
203205
*/
204206
@Bean
205207
@ConditionalOnMissingBean
206208
DataRestOperationService dataRestOperationBuilder(DataRestRequestService dataRestRequestService, DataRestTagsService tagsBuilder,
207-
DataRestResponseService dataRestResponseService) {
208-
return new DataRestOperationService(dataRestRequestService, tagsBuilder, dataRestResponseService);
209+
DataRestResponseService dataRestResponseService, OperationService operationService) {
210+
return new DataRestOperationService(dataRestRequestService, tagsBuilder, dataRestResponseService,operationService);
209211
}
210212

211213
/**

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

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
package org.springdoc.data.rest.core;
2525

2626
import java.lang.reflect.Field;
27+
import java.lang.reflect.Method;
2728
import java.util.Arrays;
2829

2930
import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -35,16 +36,20 @@
3536
import org.slf4j.Logger;
3637
import org.slf4j.LoggerFactory;
3738
import org.springdoc.core.MethodAttributes;
39+
import org.springdoc.core.OperationService;
3840
import org.springdoc.core.SpringDocAnnotationsUtils;
3941

4042
import org.springframework.core.MethodParameter;
43+
import org.springframework.core.annotation.AnnotatedElementUtils;
44+
import org.springframework.data.repository.query.Param;
4145
import org.springframework.data.rest.core.mapping.MethodResourceMapping;
4246
import org.springframework.data.rest.core.mapping.ParameterMetadata;
4347
import org.springframework.data.rest.core.mapping.ParametersMetadata;
4448
import org.springframework.data.rest.core.mapping.ResourceDescription;
4549
import org.springframework.data.rest.core.mapping.ResourceMetadata;
4650
import org.springframework.data.rest.core.mapping.TypedResourceDescription;
4751
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
52+
import org.springframework.util.CollectionUtils;
4853
import org.springframework.web.bind.annotation.RequestMethod;
4954
import org.springframework.web.method.HandlerMethod;
5055

@@ -79,6 +84,11 @@ public class DataRestOperationService {
7984
*/
8085
private DataRestResponseService dataRestResponseService;
8186

87+
/**
88+
* The Operation service.
89+
*/
90+
private OperationService operationService;
91+
8292
/**
8393
* Instantiates a new Data rest operation builder.
8494
*
@@ -87,10 +97,11 @@ public class DataRestOperationService {
8797
* @param dataRestResponseService the data rest response builder
8898
*/
8999
public DataRestOperationService(DataRestRequestService dataRestRequestService, DataRestTagsService tagsBuilder,
90-
DataRestResponseService dataRestResponseService) {
100+
DataRestResponseService dataRestResponseService, OperationService operationService) {
91101
this.dataRestRequestService = dataRestRequestService;
92102
this.tagsBuilder = tagsBuilder;
93103
this.dataRestResponseService = dataRestResponseService;
104+
this.operationService = operationService;
94105
}
95106

96107
/**
@@ -167,8 +178,25 @@ private Operation buildSearchOperation(HandlerMethod handlerMethod, DataRestRepo
167178
MethodResourceMapping methodResourceMapping) {
168179
Class<?> domainType = dataRestRepository.getDomainType();
169180
Operation operation = initOperation(handlerMethod, domainType, requestMethod);
170-
// Make schema as string if empty
181+
182+
// Add support for operation annotation
183+
io.swagger.v3.oas.annotations.Operation apiOperation = AnnotatedElementUtils.findMergedAnnotation(methodResourceMapping.getMethod(),
184+
io.swagger.v3.oas.annotations.Operation.class);
185+
186+
if (apiOperation != null)
187+
operationService.parse(apiOperation, operation, openAPI, methodAttributes);
188+
171189
ParametersMetadata parameterMetadata = methodResourceMapping.getParametersMetadata();
190+
Method method = methodResourceMapping.getMethod();
191+
192+
if (!CollectionUtils.isEmpty(parameterMetadata.getParameterNames())) {
193+
HandlerMethod repositoryHandlerMethod = new HandlerMethod(methodResourceMapping.getMethod().getDeclaringClass(), methodResourceMapping.getMethod());
194+
MethodParameter[] parameters = repositoryHandlerMethod.getMethodParameters();
195+
for (MethodParameter methodParameter : parameters) {
196+
dataRestRequestService.buildCommonParameters(domainType, openAPI, requestMethod, methodAttributes, operation, new String[] { methodParameter.getParameterName() }, new MethodParameter[] { methodParameter });
197+
}
198+
}
199+
172200
for (ParameterMetadata parameterMetadatum : parameterMetadata) {
173201
String pName = parameterMetadatum.getName();
174202
ResourceDescription description = parameterMetadatum.getDescription();
@@ -184,10 +212,13 @@ private Operation buildSearchOperation(HandlerMethod handlerMethod, DataRestRepo
184212
type = String.class;
185213
}
186214
Schema<?> schema = SpringDocAnnotationsUtils.resolveSchemaFromType(type, openAPI.getComponents(), null, null);
187-
Parameter parameter = new Parameter().name(pName).in(ParameterIn.QUERY.toString()).schema(schema);
215+
Parameter parameter = getParameterFromAnnotations(openAPI, methodAttributes, method, pName);
216+
if (parameter == null)
217+
parameter = new Parameter().name(pName).in(ParameterIn.QUERY.toString()).schema(schema);
188218
operation.addParametersItem(parameter);
189219
}
190220
}
221+
191222
if (methodResourceMapping.isPagingResource()) {
192223
MethodParameter[] parameters = handlerMethod.getMethodParameters();
193224
Arrays.stream(parameters).filter(methodParameter -> DefaultedPageable.class.equals(methodParameter.getParameterType())).findAny()
@@ -198,6 +229,32 @@ private Operation buildSearchOperation(HandlerMethod handlerMethod, DataRestRepo
198229
return operation;
199230
}
200231

232+
/**
233+
* Update parameter from annotations parameter.
234+
*
235+
* @param openAPI the open api
236+
* @param methodAttributes the method attributes
237+
* @param method the method
238+
* @param pName the p name
239+
* @return the parameter
240+
*/
241+
private Parameter getParameterFromAnnotations(OpenAPI openAPI, MethodAttributes methodAttributes, Method method, String pName) {
242+
Parameter parameter = null;
243+
for (java.lang.reflect.Parameter reflectParameter : method.getParameters()) {
244+
Param paramAnnotation = reflectParameter.getAnnotation(Param.class);
245+
if (paramAnnotation!=null && paramAnnotation.value().equals(pName)) {
246+
io.swagger.v3.oas.annotations.Parameter parameterDoc = AnnotatedElementUtils.findMergedAnnotation(
247+
AnnotatedElementUtils.forAnnotations(reflectParameter.getAnnotations()),
248+
io.swagger.v3.oas.annotations.Parameter.class);
249+
if (parameterDoc != null && (!parameterDoc.hidden() || parameterDoc.schema().hidden())) {
250+
parameter = dataRestRequestService.buildParameterFromDoc(parameterDoc, openAPI.getComponents(), methodAttributes.getJsonViewAnnotation());
251+
parameter.setName(pName);
252+
}
253+
}
254+
}
255+
return parameter;
256+
}
257+
201258
/**
202259
* Init operation operation.
203260
*

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
import java.util.Objects;
3030
import java.util.Optional;
3131

32+
import com.fasterxml.jackson.annotation.JsonView;
3233
import io.swagger.v3.oas.annotations.enums.ParameterIn;
34+
import io.swagger.v3.oas.models.Components;
3335
import io.swagger.v3.oas.models.OpenAPI;
3436
import io.swagger.v3.oas.models.Operation;
3537
import io.swagger.v3.oas.models.media.Schema;
@@ -165,6 +167,18 @@ else if (methodParameter.getParameterAnnotation(BackendId.class) != null) {
165167
}
166168
}
167169

170+
/**
171+
* Build parameter from doc parameter.
172+
*
173+
* @param parameterDoc the parameter doc
174+
* @param components the components
175+
* @param jsonViewAnnotation the json view annotation
176+
* @return the parameter
177+
*/
178+
public Parameter buildParameterFromDoc(io.swagger.v3.oas.annotations.Parameter parameterDoc, Components components, JsonView jsonViewAnnotation) {
179+
return parameterBuilder.buildParameterFromDoc(parameterDoc, components, jsonViewAnnotation);
180+
}
181+
168182
/**
169183
* Is param to ignore boolean.
170184
*

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.lang.reflect.ParameterizedType;
2727
import java.lang.reflect.Type;
2828
import java.lang.reflect.WildcardType;
29+
import java.util.Objects;
30+
import java.util.Set;
2931

3032
import io.swagger.v3.oas.models.OpenAPI;
3133
import io.swagger.v3.oas.models.Operation;
@@ -47,6 +49,7 @@
4749
import org.springframework.http.HttpStatus;
4850
import org.springframework.http.ResponseEntity;
4951
import org.springframework.util.ClassUtils;
52+
import org.springframework.util.CollectionUtils;
5053
import org.springframework.web.bind.annotation.RequestMethod;
5154
import org.springframework.web.method.HandlerMethod;
5255

@@ -83,13 +86,22 @@ public DataRestResponseService(GenericResponseService genericResponseService) {
8386
public void buildSearchResponse(Operation operation, HandlerMethod handlerMethod, OpenAPI openAPI,
8487
MethodResourceMapping methodResourceMapping, Class<?> domainType, MethodAttributes methodAttributes) {
8588
MethodParameter methodParameterReturn = handlerMethod.getReturnType();
86-
ApiResponses apiResponses = new ApiResponses();
87-
ApiResponse apiResponse = new ApiResponse();
88-
Type returnType = findSearchReturnType(handlerMethod, methodResourceMapping, domainType);
89-
Content content = genericResponseService.buildContent(openAPI.getComponents(), methodParameterReturn.getParameterAnnotations(), methodAttributes.getMethodProduces(), null, returnType);
90-
apiResponse.setContent(content);
91-
addResponse200(apiResponses, apiResponse);
92-
addResponse404(apiResponses);
89+
ApiResponses apiResponses;
90+
// check if @ApiResponse(s) is available on from @Operation annotation or method level
91+
Set<io.swagger.v3.oas.annotations.responses.ApiResponse> responsesArray = genericResponseService.getApiResponses(methodResourceMapping.getMethod());
92+
if (!responsesArray.isEmpty() || !CollectionUtils.isEmpty(operation.getResponses())) {
93+
apiResponses = genericResponseService.build(openAPI.getComponents(), new HandlerMethod(methodResourceMapping.getMethod().getDeclaringClass(), methodResourceMapping.getMethod()), operation, methodAttributes);
94+
}
95+
else {
96+
// Construct default response
97+
apiResponses = new ApiResponses();
98+
ApiResponse apiResponse = new ApiResponse();
99+
Type returnType = findSearchReturnType(handlerMethod, methodResourceMapping, domainType);
100+
Content content = genericResponseService.buildContent(openAPI.getComponents(), methodParameterReturn.getParameterAnnotations(), methodAttributes.getMethodProduces(), null, returnType);
101+
apiResponse.setContent(content);
102+
addResponse200(apiResponses, apiResponse);
103+
addResponse404(apiResponses);
104+
}
93105
operation.setResponses(apiResponses);
94106
}
95107

@@ -163,21 +175,20 @@ private void addResponse(RequestMethod requestMethod, String operationPath, ApiR
163175
* @return the type
164176
*/
165177
private Type findSearchReturnType(HandlerMethod handlerMethod, MethodResourceMapping methodResourceMapping, Class<?> domainType) {
166-
Type returnType = null;
178+
Type returnType;
167179
Type returnRepoType = ReturnTypeParser.resolveType(methodResourceMapping.getMethod().getGenericReturnType(), methodResourceMapping.getMethod().getDeclaringClass());
168180
if (methodResourceMapping.isPagingResource()) {
169181
returnType = ResolvableType.forClassWithGenerics(PagedModel.class, domainType).getType();
170182
}
171183
else if (ResolvableType.forType(returnRepoType).getRawClass() != null
172-
&& Iterable.class.isAssignableFrom(ResolvableType.forType(returnRepoType).getRawClass())) {
184+
&& Iterable.class.isAssignableFrom(Objects.requireNonNull(ResolvableType.forType(returnRepoType).getRawClass()))) {
173185
returnType = ResolvableType.forClassWithGenerics(CollectionModel.class, domainType).getType();
174186
}
175-
else if (!ClassUtils.isPrimitiveOrWrapper(domainType)) {
176-
returnType = ResolvableType.forClassWithGenerics(EntityModel.class, domainType).getType();
187+
else if (ClassUtils.isPrimitiveOrWrapper(methodResourceMapping.getReturnedDomainType())) {
188+
returnType = methodResourceMapping.getReturnedDomainType();
177189
}
178-
if (returnType == null) {
179-
returnType = ReturnTypeParser.resolveType(handlerMethod.getMethod().getGenericReturnType(), handlerMethod.getBeanType());
180-
returnType = getType(returnType, domainType);
190+
else {
191+
returnType = ResolvableType.forClassWithGenerics(EntityModel.class, domainType).getType();
181192
}
182193
return returnType;
183194
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * * Copyright 2019-2020 the original author or authors.
6+
* * * *
7+
* * * * Licensed under the Apache License, Version 2.0 (the "License");
8+
* * * * you may not use this file except in compliance with the License.
9+
* * * * You may obtain a copy of the License at
10+
* * * *
11+
* * * * https://www.apache.org/licenses/LICENSE-2.0
12+
* * * *
13+
* * * * Unless required by applicable law or agreed to in writing, software
14+
* * * * distributed under the License is distributed on an "AS IS" BASIS,
15+
* * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* * * * See the License for the specific language governing permissions and
17+
* * * * limitations under the License.
18+
* * *
19+
* *
20+
*
21+
*
22+
*/
23+
24+
package test.org.springdoc.api.app21;
25+
26+
import javax.persistence.Entity;
27+
import javax.persistence.GeneratedValue;
28+
import javax.persistence.GenerationType;
29+
import javax.persistence.Id;
30+
31+
@Entity
32+
public class Person {
33+
34+
@Id
35+
@GeneratedValue(strategy = GenerationType.AUTO)
36+
private long id;
37+
38+
private String firstName;
39+
private String lastName;
40+
41+
public String getFirstName() {
42+
return firstName;
43+
}
44+
45+
public void setFirstName(String firstName) {
46+
this.firstName = firstName;
47+
}
48+
49+
public String getLastName() {
50+
return lastName;
51+
}
52+
53+
public void setLastName(String lastName) {
54+
this.lastName = lastName;
55+
}
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * * Copyright 2019-2020 the original author or authors.
6+
* * * *
7+
* * * * Licensed under the Apache License, Version 2.0 (the "License");
8+
* * * * you may not use this file except in compliance with the License.
9+
* * * * You may obtain a copy of the License at
10+
* * * *
11+
* * * * https://www.apache.org/licenses/LICENSE-2.0
12+
* * * *
13+
* * * * Unless required by applicable law or agreed to in writing, software
14+
* * * * distributed under the License is distributed on an "AS IS" BASIS,
15+
* * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* * * * See the License for the specific language governing permissions and
17+
* * * * limitations under the License.
18+
* * *
19+
* *
20+
*
21+
*
22+
*/
23+
24+
package test.org.springdoc.api.app21;
25+
26+
import java.util.List;
27+
28+
import io.swagger.v3.oas.annotations.Operation;
29+
import io.swagger.v3.oas.annotations.Parameter;
30+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
31+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
32+
33+
import org.springframework.data.repository.PagingAndSortingRepository;
34+
import org.springframework.data.repository.query.Param;
35+
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
36+
37+
@RepositoryRestResource(collectionResourceRel = "people", path = "peopleme")
38+
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
39+
40+
@Operation(description = "this is my test")
41+
@ApiResponses(
42+
value = {
43+
@ApiResponse(responseCode = "200", description = "successful operation"),
44+
@ApiResponse(responseCode = "400", description = "Invalid ID supplied"),
45+
@ApiResponse(responseCode = "404", description = "Contact not found"),
46+
@ApiResponse(responseCode = "405", description = "Validation exception") }
47+
)
48+
List<Person> findByLastName(@Param("lastName") String name);
49+
50+
@Operation(description = "this is another test", responses = {
51+
@ApiResponse(responseCode = "200", description = "another successful operation"),
52+
@ApiResponse(responseCode = "404", description = "another Contact not found") }
53+
)
54+
List<Person> findByFirstName(@Param("firstName") @Parameter(description = "this is for first Name") String name);
55+
56+
}

0 commit comments

Comments
 (0)