Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- #3328, #3337 – `/v3/api-docs` fails with a `NullPointerException` when spring-hateoas is on the classpath without `HateoasProperties`
- #3320 – `@Order` and `Ordered` ignored when applying customizers
- #3319 – A `Page` nested in another schema is not replaced by `PagedModel`
- #3313 – Springdoc auto-configurations rely on unspecified auto-configuration ordering
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.hateoas.autoconfigure.HateoasProperties;
import org.springframework.context.annotation.Bean;
Expand All @@ -58,23 +59,62 @@
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(SpringDocConfiguration.class)
@ConditionalOnExpression("${springdoc.api-docs.enabled:true} and ${springdoc.enable-hateoas:true}")
@ConditionalOnClass({LinkRelationProvider.class, HateoasProperties.class})
@ConditionalOnClass(LinkRelationProvider.class)
@ConditionalOnWebApplication
@ConditionalOnBean(SpringDocConfiguration.class)
public class SpringDocHateoasConfiguration {

/**
* Hateoas hal provider hateoas hal provider.
* Configuration for HateoasHalProvider when HateoasProperties is on the classpath.
*
* @param hateoasPropertiesOptional the hateoas properties optional
* @param objectMapperProvider the object mapper provider
* @return the hateoas hal provider
* @author bnasslahsen
*/
@Bean
@ConditionalOnMissingBean
@Lazy(false)
HateoasHalProvider hateoasHalProvider(Optional<HateoasProperties> hateoasPropertiesOptional, ObjectMapperProvider objectMapperProvider) {
return new HateoasHalProvider(hateoasPropertiesOptional, objectMapperProvider);
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = {
"org.springframework.hateoas.server.LinkRelationProvider",
"org.springframework.boot.hateoas.autoconfigure.HateoasProperties"
})
static class HateoasPropertiesConfiguration {

/**
* Hateoas hal provider hateoas hal provider.
*
* @param hateoasPropertiesOptional the hateoas properties optional
* @param objectMapperProvider the object mapper provider
* @return the hateoas hal provider
*/
@Bean
@ConditionalOnMissingBean
@Lazy(false)
HateoasHalProvider hateoasHalProvider(Optional<HateoasProperties> hateoasPropertiesOptional, ObjectMapperProvider objectMapperProvider) {
return new HateoasHalProvider(hateoasPropertiesOptional, objectMapperProvider);
}

}

/**
* Fallback configuration for HateoasHalProvider when HateoasProperties is absent.
*
* @author bnasslahsen
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.springframework.hateoas.server.LinkRelationProvider")
@ConditionalOnMissingClass("org.springframework.boot.hateoas.autoconfigure.HateoasProperties")
static class NoHateoasPropertiesConfiguration {

/**
* Hateoas hal provider hateoas hal provider.
*
* @param objectMapperProvider the object mapper provider
* @return the hateoas hal provider
*/
@Bean
@ConditionalOnMissingBean
@Lazy(false)
HateoasHalProvider hateoasHalProvider(ObjectMapperProvider objectMapperProvider) {
return new HateoasHalProvider(Optional.empty(), objectMapperProvider);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ private void updateRequestBodySchemaProperties(String key, Schema referencedSche
* @return the schema
*/
private Schema updateResponseSchema(String className, Schema existingSchema, Components components, boolean openapi31) {
if (existingSchema == null)
return null;
Map<String, Schema> properties = existingSchema.getProperties();
EntityInfo entityInfo = entityInoMap.get(className);
if (!CollectionUtils.isEmpty(properties)) {
Expand Down Expand Up @@ -292,15 +294,16 @@ else if (EMBEDDED.equals(propId)) {
*/
private void updateResponseSchemaEmbedded(Components components, EntityInfo entityInfo, Entry<String, Schema> entry, boolean openapi31) {
String entityClassName = linkRelationProvider.getCollectionResourceRelFor(entityInfo.getDomainType()).value();
Map<String, Schema> embeddedProperties = entry.getValue().getProperties();
if (CollectionUtils.isEmpty(embeddedProperties))
return;
Schema itemsSchema = null;
if (openapi31) {
JsonSchema jsonSchema = (JsonSchema) entry.getValue().getProperties().get(entityClassName);
if (jsonSchema != null)
if (embeddedProperties.get(entityClassName) instanceof JsonSchema jsonSchema)
itemsSchema = jsonSchema.getItems();
}
else {
ArraySchema arraySchema = (ArraySchema) entry.getValue().getProperties().get(entityClassName);
if (arraySchema != null)
if (embeddedProperties.get(entityClassName) instanceof ArraySchema arraySchema)
itemsSchema = arraySchema.getItems();
}
if (itemsSchema != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package org.springdoc.core.configuration;

import org.junit.jupiter.api.Test;
import org.springdoc.core.configuration.SpringDocHateoasConfiguration.HateoasPropertiesConfiguration;
import org.springdoc.core.configuration.SpringDocHateoasConfiguration.NoHateoasPropertiesConfiguration;
import org.springdoc.core.converters.CollectionModelContentConverter;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.OpenApiHateoasLinksCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.HateoasHalProvider;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.hateoas.autoconfigure.HateoasProperties;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.hateoas.config.HateoasConfiguration;
Expand Down Expand Up @@ -82,4 +88,50 @@ void linksSchemaCustomizerShouldNotBeRegisteredIfBeanWithSameNameAlreadyExists()
assertThat(context.getBean(LINKS_SCHEMA_CUSTOMIZER)).isNotExactlyInstanceOf(OpenApiHateoasLinksCustomizer.class);
});
}

@Test
void halBeansShouldBeRegisteredWhenHateoasPropertiesIsPresent() {
hateoasContextRunner()
.run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(HateoasHalProvider.class);
assertThat(context).hasSingleBean(CollectionModelContentConverter.class);
assertThat(context).hasSingleBean(HateoasPropertiesConfiguration.class);
assertThat(context).doesNotHaveBean(NoHateoasPropertiesConfiguration.class);
});
}

/**
* spring-hateoas can be on the classpath without the Spring Boot auto-configuration
* module that carries {@link HateoasProperties}, which is what happens with
* spring-boot-starter-data-rest alone. The HAL beans, and above all the
* {@code _embedded} converter, must still be registered then.
*/
@Test
void halBeansShouldBeRegisteredWhenHateoasPropertiesIsAbsent() {
hateoasContextRunner()
.withClassLoader(new FilteredClassLoader(HateoasProperties.class))
.run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(HateoasHalProvider.class);
assertThat(context).hasSingleBean(CollectionModelContentConverter.class);
assertThat(context).hasSingleBean(NoHateoasPropertiesConfiguration.class);
assertThat(context).doesNotHaveBean(HateoasPropertiesConfiguration.class);
});
}

private WebApplicationContextRunner hateoasContextRunner() {
return new WebApplicationContextRunner()
.withPropertyValues(
"springdoc.api-docs.enabled=true",
"springdoc.enable-hateoas=true"
)
.withConfiguration(AutoConfigurations.of(
WebMvcAutoConfiguration.class,
HateoasConfiguration.class,
SpringDocConfiguration.class,
SpringDocConfigProperties.class,
SpringDocHateoasConfiguration.class
));
}
}
Loading