1
2
3
4
5
6
7 package org.tailormap.api.configuration;
8
9 import com.fasterxml.jackson.annotation.JsonTypeInfo;
10 import java.lang.invoke.MethodHandles;
11 import java.nio.charset.StandardCharsets;
12 import org.jspecify.annotations.NonNull;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15 import org.springframework.beans.factory.BeanClassLoaderAware;
16 import org.springframework.context.annotation.Bean;
17 import org.springframework.context.annotation.Configuration;
18 import org.springframework.core.convert.ConversionService;
19 import org.springframework.core.convert.support.GenericConversionService;
20 import org.springframework.security.jackson.SecurityJacksonModules;
21 import org.springframework.session.config.SessionRepositoryCustomizer;
22 import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
23 import tools.jackson.core.JacksonException;
24 import tools.jackson.core.StreamReadFeature;
25 import tools.jackson.databind.DefaultTyping;
26 import tools.jackson.databind.SerializationFeature;
27 import tools.jackson.databind.json.JsonMapper;
28 import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
29
30 @Configuration(proxyBeanMethods = false)
31 public class JdbcSessionConfiguration implements BeanClassLoaderAware {
32 private static final Logger logger =
33 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
34
35 private static final String CREATE_SESSION_ATTRIBUTE_QUERY = """
36 INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
37 VALUES (?, ?, convert_from(?, 'UTF8')::jsonb)
38 """;
39
40 private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
41 UPDATE %TABLE_NAME%_ATTRIBUTES
42 SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
43 WHERE SESSION_PRIMARY_ID = ?
44 AND ATTRIBUTE_NAME = ?
45 """;
46
47 private ClassLoader classLoader;
48
49 @Bean
50 SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
51 return (sessionRepository) -> {
52 sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
53 sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
54 };
55 }
56
57 @Bean("springSessionConversionService")
58 public ConversionService springSessionConversionService() {
59
60 BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder()
61 .allowIfSubType("org.tailormap.api.security.")
62 .allowIfSubType("org.springframework.security.")
63 .allowIfSubType("java.util.")
64 .allowIfSubType(java.lang.Number.class)
65 .allowIfSubType("java.time.")
66 .allowIfBaseType(Object.class);
67
68 JsonMapper mapper = JsonMapper.builder()
69 .configure(
70 StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION,
71 (logger.isDebugEnabled() || logger.isTraceEnabled()))
72 .configure(SerializationFeature.INDENT_OUTPUT, (logger.isDebugEnabled() || logger.isTraceEnabled()))
73
74
75
76 .addMixIn(
77 org.tailormap.api.security.TailormapUserDetailsImpl.class,
78 org.tailormap.api.security.TailormapUserDetailsImplMixin.class)
79 .addMixIn(
80 org.tailormap.api.security.TailormapOidcUser.class,
81 org.tailormap.api.security.TailormapOidcUserMixin.class)
82 .addModules(SecurityJacksonModules.getModules(this.classLoader, builder))
83 .activateDefaultTyping(builder.build(), DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)
84 .build();
85
86 final GenericConversionService converter = new GenericConversionService();
87
88 converter.addConverter(Object.class, byte[].class, source -> {
89 try {
90 logger.debug("Serializing Spring Session: {}", source);
91 return mapper.writerFor(Object.class).writeValueAsBytes(source);
92 } catch (JacksonException e) {
93 logger.error("Error serializing Spring Session object: {}", source, e);
94 throw new RuntimeException("Unable to serialize Spring Session.", e);
95 }
96 });
97
98 converter.addConverter(byte[].class, Object.class, source -> {
99 try {
100 logger.debug(
101 "Deserializing Spring Session from bytes, length: {} ({})",
102 source.length,
103 new String(source, StandardCharsets.UTF_8));
104 return mapper.readValue(source, Object.class);
105 } catch (JacksonException e) {
106 String preview;
107 try {
108 String content = new String(source, StandardCharsets.UTF_8);
109 int maxLength = 256;
110 preview = content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
111 } catch (Exception ex) {
112 preview = "<unavailable>";
113 }
114 logger.error(
115 "Error deserializing Spring Session from bytes, length: {}, preview: {}",
116 source.length,
117 preview,
118 e);
119 throw new RuntimeException("Unable to deserialize Spring Session.", e);
120 }
121 });
122
123 return converter;
124 }
125
126 @Override
127 public void setBeanClassLoader(@NonNull ClassLoader classLoader) {
128 this.classLoader = classLoader;
129 }
130 }