Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add model autoconfigure #110

Merged
merged 29 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bdad6af
Add statement repository to xAPI Server
Mar 29, 2023
b035444
remove country-code check from LocaleValidator
Selindek Mar 31, 2023
8588584
add CoercionInputShape.EmptyObject in ServerConfig
Selindek Mar 31, 2023
fc808a4
limit number of returned statements
Selindek Mar 31, 2023
8978aa3
refactor LocalValidator -> All CTS 'language code' tests pass
Selindek Mar 31, 2023
4afcae0
put back objectType property for testing invalid values
Selindek Mar 31, 2023
1d3f512
validate negative zero timestamp offsets
Selindek Mar 31, 2023
272861b
Merge branch 'main' into polish-xapi-server-sample
thomasturrell Mar 31, 2023
59b78b6
refactor configuration
Selindek Apr 3, 2023
49dc709
Merge branch 'polish-xapi-server-sample' into polish-polish
thomasturrell Apr 3, 2023
023256c
Merge remote-tracking branch 'origin/main' into polish-polish
Apr 3, 2023
bd6d0e6
Update pom.xml
thomasturrell Apr 3, 2023
56286c2
fix
Apr 3, 2023
d9b9175
fcs
Selindek Apr 4, 2023
9bf404c
Merge branch 'polish-polish' of https://github.com/BerryCloud/xapi-ja…
Selindek Apr 4, 2023
328a507
fixes for comments
Selindek Apr 4, 2023
e7d13eb
roll back strict objectType check
Selindek Apr 4, 2023
6b452ac
fix test
Selindek Apr 4, 2023
fcddfe3
fsi
Selindek Apr 4, 2023
c1362ee
polish pom
Apr 4, 2023
3e4a6b6
Merge branch 'polish-polish' of https://github.com/BerryCloud/xapi-ja…
Apr 4, 2023
d0fc644
change copyright dates
Apr 4, 2023
7e9d2f2
fix xAPI case
Apr 4, 2023
8504936
Update samples/xapi-server/src/main/resources/application.properties
thomasturrell Apr 4, 2023
db7430d
Add tests for StrictTimestampDeserializer
Apr 4, 2023
44d7b8d
Merge branch 'polish-polish' of https://github.com/BerryCloud/xapi-ja…
Apr 4, 2023
5446f70
correct test names
Apr 4, 2023
2d0dbe4
add test
Apr 4, 2023
0942217
tidy up wording
Apr 4, 2023
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
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<module>xapi-model</module>
<module>xapi-client</module>
<module>samples</module>
<module>xapi-model-spring-boot-autoconfigure</module>
</modules>
<build>
<pluginManagement>
Expand Down Expand Up @@ -262,6 +263,11 @@
<artifactId>xapi-model</artifactId>
<version>1.1.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-model-spring-boot-autoconfigure</artifactId>
<version>1.1.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void run(String... args) throws Exception {

// If the attachment parameter is set to true in a getStatement (or a getStatements) request
// then the server will send the response in a multipart/mixed format (even if the
// Statement doesn't have attachments.) The xApi client automatically converts these responses
// Statement doesn't have attachments.) The xAPI client automatically converts these responses
// back to the regular Statement / StatementResponse format and populate the returned
// statement's or statements' attachments' content from the additional parts from the response.

Expand Down
4 changes: 4 additions & 0 deletions samples/xapi-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-model</artifactId>
</dependency>
<dependency>
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-model-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public StatementResult getStatements() {

// add custom logic here...

final var statements = StreamSupport.stream(repository.findAll().spliterator(), false)
final var statements = StreamSupport.stream(repository.findAll().spliterator(), false).limit(10)
.map(e -> convertToStatement(e)).toList();

return StatementResult.builder().statements(statements).more(URI.create("")).build();
Expand Down
18 changes: 14 additions & 4 deletions samples/xapi-server/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
# Jackson deserialization configuration
spring.jackson.deserialization.ACCEPT_SINGLE_VALUE_AS_ARRAY = true
spring.jackson.deserialization.FAIL_ON_UNKNOWN_PROPERTIES = true
spring.jackson.deserialization.FAIL_ON_TRAILING_TOKENS = true
# The xAPI specification has extremely strict rules for API requests/responses formatting.
# Some activity providers do not conform to these rules.
#
# In some cases it may be desirable to turn off some or all of the rules in order to be compatible
# with a wider range of xAPI activity providers. However, doing this is in violation of the xAPI
# specification.

xApi.model.strictProperties = true
xApi.model.strictJson = true

xApi.model.strictLocale = true
xApi.model.strictTimestamp = true
xApi.model.strictNullValues = true
xApi.model.strictLiterals = true
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/*
* Copyright 2016-2019 Berry Cloud Ltd. All rights reserved.
* Copyright 2016-2023 Berry Cloud Ltd. All rights reserved.
*/

package dev.learning.xapi.samples.xapiserver;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
Expand All @@ -21,7 +22,8 @@
* @author Thomas Turrell-Croft
* @author István Rátkai (Selindek)
*/
@WebMvcTest(value = StatementController.class)
@WebMvcTest(value = {StatementController.class},
properties = "spring.jackson.deserialization.ACCEPT_SINGLE_VALUE_AS_ARRAY = true")
class StatementControllerTest {

@Autowired
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 Berry Cloud Ltd. All rights reserved.
* Copyright 2016-2023 Berry Cloud Ltd. All rights reserved.
*/

package dev.learning.xapi.client.configuration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Xapi client properties.
* XapiClient properties.
*
* @author István Rátkai (Selindek)
*/
Expand Down
37 changes: 37 additions & 0 deletions xapi-model-spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-build</artifactId>
<version>1.1.3-SNAPSHOT</version>
</parent>
<artifactId>xapi-model-spring-boot-autoconfigure</artifactId>
<name>xAPI Spring Boot Autoconfigure</name>
<description>learning.dev Spring Boot Autoconfigure</description>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-model</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2016-2023 Berry Cloud Ltd. All rights reserved.
*/

package dev.learning.xapi;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.fasterxml.jackson.databind.type.LogicalType;
import dev.learning.xapi.jackson.XapiStrictLocaleModule;
import dev.learning.xapi.jackson.XapiStrictNullValuesModule;
import dev.learning.xapi.jackson.XapiStrictTimestampModule;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* XapiModelAutoConfiguration.
*
* @author István Rátkai (Selindek)
*/
@Configuration
@AutoConfigureBefore(value = JacksonProperties.class)
public class XapiModelAutoConfiguration {

/**
* SingleValueArrayCustomizer.
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer singleValueArrayCustomizer() {
return builder -> builder.postConfigurer(objectMapper ->
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
);
}

/**
* StrictLocaleCustomizer.
*/
@Bean
@ConditionalOnProperty(name = "xApi.model.strictLocale", havingValue = "true",
matchIfMissing = true)
public Jackson2ObjectMapperBuilderCustomizer strictLocaleCustomizer() {
return builder -> builder.postConfigurer(objectMapper ->
objectMapper.registerModule(new XapiStrictLocaleModule())
);
}

/**
* StrictTimestampCustomizer.
*/
@Bean
@ConditionalOnProperty(name = "xApi.model.strictTimestamp", havingValue = "true",
matchIfMissing = true)
public Jackson2ObjectMapperBuilderCustomizer strictTimestampCustomizer() {
return builder -> builder.postConfigurer(objectMapper ->
objectMapper.registerModule(new XapiStrictTimestampModule())
);
}

/**
* StrictNullValuesCustomizer.
*/
@Bean
@ConditionalOnProperty(name = "xApi.model.strictNullValues", havingValue = "true",
matchIfMissing = true)
public Jackson2ObjectMapperBuilderCustomizer strictNullValuesCustomizer() {
return builder -> builder.postConfigurer(objectMapper ->
objectMapper.registerModule(new XapiStrictNullValuesModule())
);
}

/**
* SstrictPropertiesCustomizer.
*/
@Bean
@ConditionalOnProperty(name = "xApi.model.strictProperties", havingValue = "true",
matchIfMissing = true)
public Jackson2ObjectMapperBuilderCustomizer strictPropertiesCustomizer() {
return builder -> builder.postConfigurer(objectMapper ->
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
);
}

/**
* StrictJsonCustomizer.
*/
@Bean
@ConditionalOnProperty(name = "xApi.model.strictJson", havingValue = "true",
matchIfMissing = true)
public Jackson2ObjectMapperBuilderCustomizer strictJsonCustomizer() {
return builder -> builder.postConfigurer(objectMapper ->
objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true)
);
}

/**
* StrictLiteralsCustomizer.
*/
@Bean
@ConditionalOnProperty(name = "xApi.model.strictLiterals", havingValue = "true",
matchIfMissing = true)
public Jackson2ObjectMapperBuilderCustomizer strictLiteralsCustomizer() {
return builder -> builder.postConfigurer(objectMapper -> {

objectMapper.coercionConfigFor(LogicalType.Boolean)

.setCoercion(CoercionInputShape.String, CoercionAction.Fail);

objectMapper.coercionConfigFor(LogicalType.Textual)

.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail);

objectMapper.coercionConfigFor(LogicalType.Float)

.setCoercion(CoercionInputShape.String, CoercionAction.Fail);

objectMapper.coercionConfigFor(LogicalType.Integer)

.setCoercion(CoercionInputShape.String, CoercionAction.Fail);

});

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dev.learning.xapi.XapiModelAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2016-2023 Berry Cloud Ltd. All rights reserved.
*/

package dev.learning.xapi.jackson;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;

/**
* Deserialization Modifier for restricting <code>null</code> literals in Statements.
*
* @author István Rátkai (Selindek)
*/
public class NotNullDeserializationModifier extends BeanDeserializerModifier {

/**
* {@inheritDoc}
*/
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc, JsonDeserializer<?> deserializer) {

// Use standard deserializer on explicit Object class. it happens only in Extensions
if (beanDesc.getBeanClass() == Object.class) {
return deserializer;
}
return new NotNullDeserializer<>(deserializer, beanDesc.getBeanClass());
}

private static class NotNullDeserializer<T> extends StdDeserializer<T>
implements ResolvableDeserializer {

private static final long serialVersionUID = -3153072039006440049L;
// This field is not Serializable, but this class is never serialized.
private final JsonDeserializer<T> defaultDeserializer; // NOSONAR

private NotNullDeserializer(JsonDeserializer<T> defaultDeserializer, Class<?> vc) {
super(vc);
this.defaultDeserializer = defaultDeserializer;

}

/**
* {@inheritDoc}
*/
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return defaultDeserializer.deserialize(p, ctxt);
}

/**
* {@inheritDoc}
*/
@Override
thomasturrell marked this conversation as resolved.
Show resolved Hide resolved
public T getNullValue(DeserializationContext ctxt) throws JsonMappingException {
throw ctxt.instantiationException(defaultDeserializer.handledType(),
"null literal is not allowed");
}

/**
* {@inheritDoc}
*/
@Override
public T getAbsentValue(DeserializationContext ctxt) throws JsonMappingException {
return null;
}

/**
* {@inheritDoc}
*/
@Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
if (defaultDeserializer instanceof final ResolvableDeserializer resolvableDeserializer) {
resolvableDeserializer.resolve(ctxt);
}
}

}
}
Loading