1
2
3
4
5
6
7 package nl.b3p.planmonitorwonen.api.configuration;
8
9 import com.fasterxml.jackson.annotation.JsonTypeInfo;
10 import java.lang.invoke.MethodHandles;
11 import java.nio.charset.StandardCharsets;
12 import javax.sql.DataSource;
13 import org.jspecify.annotations.NonNull;
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16 import org.springframework.beans.factory.BeanClassLoaderAware;
17 import org.springframework.beans.factory.annotation.Qualifier;
18 import org.springframework.beans.factory.annotation.Value;
19 import org.springframework.boot.jdbc.DataSourceBuilder;
20 import org.springframework.context.annotation.Bean;
21 import org.springframework.context.annotation.Configuration;
22 import org.springframework.context.annotation.Profile;
23 import org.springframework.core.convert.ConversionFailedException;
24 import org.springframework.core.convert.ConversionService;
25 import org.springframework.core.convert.TypeDescriptor;
26 import org.springframework.core.convert.support.GenericConversionService;
27 import org.springframework.jdbc.core.simple.JdbcClient;
28 import org.springframework.jdbc.datasource.DataSourceTransactionManager;
29 import org.springframework.security.jackson.SecurityJacksonModules;
30 import org.springframework.session.config.SessionRepositoryCustomizer;
31 import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
32 import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
33 import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
34 import org.springframework.transaction.PlatformTransactionManager;
35 import tools.jackson.core.JacksonException;
36 import tools.jackson.core.StreamReadFeature;
37 import tools.jackson.databind.DefaultTyping;
38 import tools.jackson.databind.SerializationFeature;
39 import tools.jackson.databind.json.JsonMapper;
40 import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
41
42 @Configuration
43 @EnableJdbcHttpSession
44 @Profile("!test")
45 public class JdbcSessionConfiguration implements BeanClassLoaderAware {
46 private static final Logger logger =
47 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
48
49 @Value("${spring.datasource.url}")
50 private String dataSourceUrl;
51
52 @Value("${spring.datasource.username}")
53 private String dataSourceUsername;
54
55 @Value("${spring.datasource.password}")
56 private String dataSourcePassword;
57
58 @Value("${tailormap.datasource.url}")
59 private String sessionDataSourceUrl;
60
61 @Value("${tailormap.datasource.username}")
62 private String sessionDataSourceUsername;
63
64 @Value("${tailormap.datasource.password}")
65 private String sessionDataSourcePassword;
66
67 private static final String CREATE_SESSION_ATTRIBUTE_QUERY = """
68 INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
69 VALUES (?, ?, convert_from(?, 'UTF8')::jsonb)
70 """;
71
72 private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
73 UPDATE %TABLE_NAME%_ATTRIBUTES
74 SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
75 WHERE SESSION_PRIMARY_ID = ?
76 AND ATTRIBUTE_NAME = ?
77 """;
78
79 private ClassLoader classLoader;
80
81 @Override
82 public void setBeanClassLoader(@NonNull ClassLoader classLoader) {
83 this.classLoader = classLoader;
84 }
85
86 @Bean
87 public DataSource dataSource() {
88 DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
89 dataSourceBuilder.url(dataSourceUrl);
90 dataSourceBuilder.username(dataSourceUsername);
91 dataSourceBuilder.password(dataSourcePassword);
92 return dataSourceBuilder.build();
93 }
94
95 @Bean
96 public JdbcClient jdbcClient(DataSource dataSource) {
97 return JdbcClient.create(dataSource);
98 }
99
100 @Bean(name = "tailormapJdbcClient")
101 public JdbcClient tailormapJdbcClient(@Qualifier("tailormapDataSource") DataSource data) {
102 return JdbcClient.create(data);
103 }
104
105 @Bean
106 SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
107 return (sessionRepository) -> {
108 sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
109 sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
110 };
111 }
112
113 @Bean(name = {"springSessionDataSource", "tailormapDataSource"})
114 @SpringSessionDataSource
115 public DataSource sessionDataSource() {
116 DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
117 dataSourceBuilder.url(sessionDataSourceUrl);
118 dataSourceBuilder.username(sessionDataSourceUsername);
119 dataSourceBuilder.password(sessionDataSourcePassword);
120 return dataSourceBuilder.build();
121 }
122
123 @Bean(name = "springSessionTransactionOperations")
124 public PlatformTransactionManager springSessionTransactionOperations(
125 @Qualifier("springSessionDataSource") DataSource springSessionDatasource) {
126 return new DataSourceTransactionManager(springSessionDatasource);
127 }
128
129 @Bean("springSessionConversionService")
130 public ConversionService springSessionConversionService() {
131 BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder()
132 .allowIfSubType("org.tailormap.api.security.")
133 .allowIfSubType("org.springframework.security.")
134 .allowIfSubType("java.util.")
135 .allowIfSubType(java.lang.Number.class)
136 .allowIfSubType("java.time.")
137 .allowIfBaseType(Object.class);
138
139 JsonMapper mapper = JsonMapper.builder()
140 .configure(
141 StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION,
142 (logger.isDebugEnabled() || logger.isTraceEnabled()))
143 .configure(SerializationFeature.INDENT_OUTPUT, (logger.isDebugEnabled() || logger.isTraceEnabled()))
144 .addMixIn(
145 org.tailormap.api.security.TailormapUserDetailsImpl.class,
146 org.tailormap.api.security.TailormapUserDetailsImplMixin.class)
147 .addMixIn(
148 org.tailormap.api.security.TailormapOidcUser.class,
149 org.tailormap.api.security.TailormapOidcUserMixin.class)
150 .addModules(SecurityJacksonModules.getModules(this.classLoader, builder))
151 .activateDefaultTyping(builder.build(), DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)
152 .build();
153
154 final GenericConversionService converter = new GenericConversionService();
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169 converter.addConverter(byte[].class, Object.class, source -> {
170 try {
171 logger.debug(
172 "Deserializing Spring Session from bytes, length: {} ({})",
173 source.length,
174 new String(source, StandardCharsets.UTF_8));
175 return mapper.readValue(source, Object.class);
176 } catch (JacksonException e) {
177 String preview;
178 try {
179 String content = new String(source, StandardCharsets.UTF_8);
180 int maxLength = 256;
181 if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
182 preview = content;
183 } else {
184 preview = content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
185 }
186 } catch (Exception ex) {
187 preview = "<unavailable>";
188 }
189 logger.error(
190 "Error deserializing Spring Session from bytes, length: {}, preview: {}",
191 source.length,
192 preview,
193 e);
194 throw new ConversionFailedException(
195 TypeDescriptor.valueOf(byte[].class), TypeDescriptor.valueOf(Object.class), source, e);
196 }
197 });
198
199 return converter;
200 }
201 }