Skip to content

Commit 01992fe

Browse files
committed
HATOEAS Links produces a circular reference. Fixes #1014.
1 parent ed5f8b5 commit 01992fe

File tree

10 files changed

+245
-3
lines changed

10 files changed

+245
-3
lines changed

springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
import org.springframework.web.method.HandlerMethod;
103103

104104
import static org.springdoc.core.Constants.ACTUATOR_DEFAULT_GROUP;
105+
import static org.springdoc.core.Constants.LINKS_SCHEMA_CUSTOMISER;
105106
import static org.springdoc.core.Constants.OPERATION_ATTRIBUTE;
106107
import static org.springdoc.core.Constants.SPRING_MVC_SERVLET_PATH;
107108
import static org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.isDeprecated;
@@ -211,9 +212,12 @@ protected AbstractOpenApiResource(String groupName, ObjectFactory<OpenAPIService
211212
this.responseBuilder = responseBuilder;
212213
this.operationParser = operationParser;
213214
this.openApiCustomisers = openApiCustomisers;
215+
//add the default customizers
216+
Map<String, OpenApiCustomiser> existingOpenApiCustomisers = openAPIService.getContext().getBeansOfType(OpenApiCustomiser.class);
217+
if (!CollectionUtils.isEmpty(existingOpenApiCustomisers) && existingOpenApiCustomisers.containsKey(LINKS_SCHEMA_CUSTOMISER))
218+
openApiCustomisers.ifPresent(openApiCustomisersList -> openApiCustomisersList.add(existingOpenApiCustomisers.get(LINKS_SCHEMA_CUSTOMISER)));
214219
this.springDocConfigProperties = springDocConfigProperties;
215-
if (operationCustomizers.isPresent())
216-
operationCustomizers.get().removeIf(Objects::isNull);
220+
operationCustomizers.ifPresent(customizers -> customizers.removeIf(Objects::isNull));
217221
this.operationCustomizers = operationCustomizers;
218222
this.optionalActuatorProvider = actuatorProvider;
219223
if (springDocConfigProperties.isPreLoadingEnabled())

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ public final class Constants {
330330
*/
331331
public static final String ACTUATOR_DEFAULT_GROUP = "x-actuator";
332332

333+
/**
334+
* The constant LINKS_SCHEMA_CUSTOMISER.
335+
*/
336+
public static final String LINKS_SCHEMA_CUSTOMISER = "linksSchemaCustomiser";
333337
/**
334338
* Instantiates a new Constants.
335339
*/

springdoc-openapi-hateoas/src/main/java/org/springdoc/hateoas/SpringDocHateoasConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.hateoas.RepresentationModel;
4343
import org.springframework.hateoas.server.LinkRelationProvider;
4444

45+
import static org.springdoc.core.Constants.LINKS_SCHEMA_CUSTOMISER;
4546
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
4647

4748
/**
@@ -90,7 +91,7 @@ CollectionModelContentConverter collectionModelContentConverter(HateoasHalProvid
9091
* @return the open api customiser
9192
* @see org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer#serialize(Links, JsonGenerator, SerializerProvider) org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer#serialize(Links, JsonGenerator, SerializerProvider)org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer#serialize(Links, JsonGenerator, SerializerProvider)org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer#serialize(Links, JsonGenerator, SerializerProvider)
9293
*/
93-
@Bean
94+
@Bean(LINKS_SCHEMA_CUSTOMISER)
9495
@ConditionalOnMissingBean
9596
@Lazy(false)
9697
OpenApiCustomiser linksSchemaCustomiser(HateoasHalProvider halProvider, SpringDocConfigProperties springDocConfigProperties) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package test.org.springdoc.api.app7;
2+
3+
import org.springdoc.core.GroupedOpenApi;
4+
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
@Configuration
9+
public class FooConfiguration {
10+
@Bean
11+
public GroupedOpenApi userOpenApi() {
12+
String packagesToscan[] = {"test.org.springdoc.api.app7"};
13+
return GroupedOpenApi.builder().group("foo-service").packagesToScan(packagesToscan)
14+
.build();
15+
}
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app7;
20+
21+
import test.org.springdoc.api.AbstractSpringDocTest;
22+
23+
import org.springframework.boot.autoconfigure.SpringBootApplication;
24+
25+
public class SpringDocApp7Test extends AbstractSpringDocTest {
26+
27+
@SpringBootApplication
28+
static class SpringDocTestApp {
29+
}
30+
31+
32+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package test.org.springdoc.api.app7.application;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
6+
@Data
7+
@AllArgsConstructor
8+
public class Foo {
9+
String foo;
10+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package test.org.springdoc.api.app7.application;
2+
3+
import java.util.UUID;
4+
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.hateoas.EntityModel;
7+
import org.springframework.hateoas.MediaTypes;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
@RestController
14+
public class FooController {
15+
16+
private final FooService fooService;
17+
private final FooResourceAssembler fooResourceAssembler;
18+
19+
@Autowired
20+
public FooController(
21+
FooService fooService,
22+
FooResourceAssembler fooResourceAssembler) {
23+
this.fooService = fooService;
24+
this.fooResourceAssembler = fooResourceAssembler;
25+
}
26+
27+
@GetMapping(value = "foo/{id}", produces = MediaTypes.HAL_JSON_VALUE)
28+
public ResponseEntity<EntityModel<Foo>> getFoo(@PathVariable("id") UUID id) throws Exception {
29+
Foo foo = fooService.getFoo(id).orElseThrow(Exception::new);
30+
return ResponseEntity.ok(fooResourceAssembler.toModel(foo));
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package test.org.springdoc.api.app7.application;
2+
3+
import java.util.UUID;
4+
5+
import org.springframework.hateoas.CollectionModel;
6+
import org.springframework.hateoas.EntityModel;
7+
import org.springframework.hateoas.server.SimpleRepresentationModelAssembler;
8+
import org.springframework.stereotype.Component;
9+
10+
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
11+
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
12+
13+
@Component
14+
public class FooResourceAssembler
15+
implements SimpleRepresentationModelAssembler<Foo> {
16+
17+
@Override
18+
public void addLinks(EntityModel<Foo> resource) {
19+
Foo foo = resource.getContent();
20+
if (foo != null) {
21+
try {
22+
resource.add(
23+
linkTo(methodOn(FooController.class).getFoo(UUID.fromString(foo.getFoo()))).withSelfRel());
24+
} catch (Exception e) {
25+
e.printStackTrace();
26+
}
27+
}
28+
}
29+
30+
@Override
31+
public void addLinks(CollectionModel<EntityModel<Foo>> resources) {
32+
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package test.org.springdoc.api.app7.application;
2+
3+
import java.util.Optional;
4+
import java.util.UUID;
5+
6+
import org.springframework.stereotype.Service;
7+
8+
@Service
9+
public class FooService {
10+
public Optional<Foo> getFoo(UUID uuid) {
11+
return Optional.of(new Foo(uuid.toString()));
12+
}
13+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "http://localhost",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/foo/{id}": {
15+
"get": {
16+
"tags": [
17+
"foo-controller"
18+
],
19+
"operationId": "getFoo",
20+
"parameters": [
21+
{
22+
"name": "id",
23+
"in": "path",
24+
"required": true,
25+
"schema": {
26+
"type": "string",
27+
"format": "uuid"
28+
}
29+
}
30+
],
31+
"responses": {
32+
"200": {
33+
"description": "OK",
34+
"content": {
35+
"application/hal+json": {
36+
"schema": {
37+
"$ref": "#/components/schemas/EntityModelFoo"
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
44+
}
45+
},
46+
"components": {
47+
"schemas": {
48+
"EntityModelFoo": {
49+
"type": "object",
50+
"properties": {
51+
"foo": {
52+
"type": "string"
53+
},
54+
"_links": {
55+
"$ref": "#/components/schemas/Links"
56+
}
57+
}
58+
},
59+
"Links": {
60+
"type": "object",
61+
"additionalProperties": {
62+
"$ref": "#/components/schemas/Link"
63+
}
64+
},
65+
"Link": {
66+
"type": "object",
67+
"properties": {
68+
"href": {
69+
"type": "string"
70+
},
71+
"hreflang": {
72+
"type": "string"
73+
},
74+
"title": {
75+
"type": "string"
76+
},
77+
"type": {
78+
"type": "string"
79+
},
80+
"deprecation": {
81+
"type": "string"
82+
},
83+
"profile": {
84+
"type": "string"
85+
},
86+
"name": {
87+
"type": "string"
88+
},
89+
"templated": {
90+
"type": "boolean"
91+
}
92+
}
93+
}
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)