1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.chat;
17
18 import java.time.LocalDateTime;
19 import java.util.Iterator;
20 import java.util.Map;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.stream.Collectors;
23
24 import org.apache.logging.log4j.LogManager;
25 import org.apache.logging.log4j.Logger;
26 import org.codelibs.core.timer.TimeoutManager;
27 import org.codelibs.core.timer.TimeoutTask;
28 import org.codelibs.fess.entity.ChatMessage;
29 import org.codelibs.fess.entity.ChatSession;
30 import org.codelibs.fess.util.ComponentUtil;
31
32 import jakarta.annotation.PostConstruct;
33 import jakarta.annotation.PreDestroy;
34
35
36
37
38
39
40
41
42
43
44
45
46 public class ChatSessionManager {
47
48 private static final Logger logger = LogManager.getLogger(ChatSessionManager.class);
49
50 private final Map<String, ChatSession> sessionCache = new ConcurrentHashMap<>();
51 private TimeoutTask cleanupTask;
52
53
54
55
56 public ChatSessionManager() {
57
58 }
59
60
61
62
63 @PostConstruct
64 public void init() {
65
66 cleanupTask = TimeoutManager.getInstance().addTimeoutTarget(this::cleanupExpiredSessions, 300, true);
67 if (logger.isDebugEnabled()) {
68 logger.debug("Initialized ChatSessionManager");
69 }
70 }
71
72
73
74
75 @PreDestroy
76 public void destroy() {
77 if (cleanupTask != null && !cleanupTask.isCanceled()) {
78 cleanupTask.cancel();
79 }
80 }
81
82
83
84
85
86
87
88 public ChatSession createSession(final String userId) {
89 final ChatSession session = new ChatSession(userId);
90 sessionCache.put(session.getSessionId(), session);
91 if (logger.isDebugEnabled()) {
92 logger.debug("Created chat session: sessionId={}, userId={}", session.getSessionId(), userId);
93 }
94 enforceMaxSize();
95 return session;
96 }
97
98
99
100
101
102
103
104 private ChatSession findSession(final String sessionId) {
105 final ChatSession session = sessionCache.get(sessionId);
106 if (session == null) {
107 if (logger.isDebugEnabled()) {
108 logger.debug("Session not found. sessionId={}", sessionId);
109 }
110 return null;
111 }
112 if (isExpired(session)) {
113 sessionCache.remove(sessionId);
114 if (logger.isDebugEnabled()) {
115 logger.debug("Session expired and removed. sessionId={}, lastAccessedAt={}", sessionId, session.getLastAccessedAt());
116 }
117 return null;
118 }
119 return session;
120 }
121
122
123
124
125
126
127
128 public ChatSession getSession(final String sessionId) {
129 final ChatSession session = findSession(sessionId);
130 if (session == null) {
131 return null;
132 }
133 session.touch();
134 if (logger.isDebugEnabled()) {
135 logger.debug("Session retrieved. sessionId={}, messageCount={}", sessionId, session.getMessages().size());
136 }
137 return session;
138 }
139
140
141
142
143
144
145
146
147 public ChatSession getOrCreateSession(final String sessionId, final String userId) {
148 if (sessionId != null) {
149 final ChatSession session = findSession(sessionId);
150 if (session != null) {
151 final String sessionUserId = session.getUserId();
152
153 if (userId != null && !userId.equals(sessionUserId)) {
154 logger.warn("Session userId mismatch. sessionId={}, requestUserId={}", sessionId, userId);
155 } else if (userId == null && sessionUserId == null) {
156
157 session.touch();
158 if (logger.isDebugEnabled()) {
159 logger.debug("Reusing existing session (both userId null). sessionId={}", sessionId);
160 }
161 return session;
162 } else if (userId != null && userId.equals(sessionUserId)) {
163 session.touch();
164 if (logger.isDebugEnabled()) {
165 logger.debug("Reusing existing session. sessionId={}, userId={}", sessionId, userId);
166 }
167 return session;
168 } else {
169
170 logger.warn("Session userId mismatch (null vs non-null). sessionId={}, sessionUserId={}", sessionId, sessionUserId);
171 }
172 }
173 }
174 if (logger.isDebugEnabled()) {
175 logger.debug("Creating new session. requestedSessionId={}, userId={}", sessionId, userId);
176 }
177 return createSession(userId);
178 }
179
180
181
182
183
184
185
186
187 public boolean addMessage(final String sessionId, final ChatMessage message) {
188 final ChatSession session = getSession(sessionId);
189 if (session == null) {
190 if (logger.isDebugEnabled()) {
191 logger.debug("Cannot add message, session not found. sessionId={}", sessionId);
192 }
193 return false;
194 }
195 session.addMessage(message);
196
197
198 final int maxMessages = getMaxHistoryMessages();
199 session.trimHistory(maxMessages);
200
201 if (logger.isDebugEnabled()) {
202 logger.debug("Message added to session. sessionId={}, role={}, messageCount={}", sessionId, message.getRole(),
203 session.getMessages().size());
204 }
205 return true;
206 }
207
208
209
210
211
212
213
214
215 public boolean clearSession(final String sessionId, final String userId) {
216 final ChatSession session = findSession(sessionId);
217 if (session == null) {
218 if (logger.isDebugEnabled()) {
219 logger.debug("Cannot clear session, not found. sessionId={}", sessionId);
220 }
221 return false;
222 }
223
224 final String sessionUserId = session.getUserId();
225 if (userId != null && !userId.equals(sessionUserId)) {
226 logger.warn("Cannot clear session, userId mismatch. sessionId={}, requestUserId={}", sessionId, userId);
227 return false;
228 }
229 if (userId == null && sessionUserId != null) {
230 logger.warn("Cannot clear session, userId mismatch (null vs non-null). sessionId={}, sessionUserId={}", sessionId,
231 sessionUserId);
232 return false;
233 }
234 session.touch();
235 session.clearMessages();
236 if (logger.isDebugEnabled()) {
237 logger.debug("Session cleared. sessionId={}, userId={}", sessionId, userId);
238 }
239 return true;
240 }
241
242
243
244
245
246
247
248
249 public boolean clearSession(final String sessionId) {
250 final ChatSession session = getSession(sessionId);
251 if (session == null) {
252 if (logger.isDebugEnabled()) {
253 logger.debug("Cannot clear session, not found. sessionId={}", sessionId);
254 }
255 return false;
256 }
257 session.clearMessages();
258 if (logger.isDebugEnabled()) {
259 logger.debug("Session cleared (no ownership check). sessionId={}", sessionId);
260 }
261 return true;
262 }
263
264
265
266
267
268
269
270 public ChatSession removeSession(final String sessionId) {
271 final ChatSession session = sessionCache.remove(sessionId);
272 if (logger.isDebugEnabled()) {
273 logger.debug("Session removed. sessionId={}, found={}", sessionId, session != null);
274 }
275 return session;
276 }
277
278
279
280
281 protected void cleanupExpiredSessions() {
282 if (logger.isDebugEnabled()) {
283 logger.debug("Running chat session cleanup. currentSessionCount={}", sessionCache.size());
284 }
285
286 int removed = 0;
287 final Iterator<Map.Entry<String, ChatSession>> iterator = sessionCache.entrySet().iterator();
288 while (iterator.hasNext()) {
289 final Map.Entry<String, ChatSession> entry = iterator.next();
290 if (isExpired(entry.getValue())) {
291 iterator.remove();
292 removed++;
293 }
294 }
295
296 if (removed > 0) {
297 if (logger.isDebugEnabled()) {
298 logger.debug("Removed expired chat sessions. removedCount={}, remainingCount={}", removed, sessionCache.size());
299 }
300 }
301 }
302
303
304
305
306 protected void enforceMaxSize() {
307 final int maxSize = getMaxSessionSize();
308 synchronized (sessionCache) {
309 if (sessionCache.size() <= maxSize) {
310 return;
311 }
312
313 final int toRemove = sessionCache.size() - maxSize;
314 logger.warn("Session cache reached maximum size. Removing oldest sessions. currentSize={}, maxSize={}, removing={}",
315 sessionCache.size(), maxSize, toRemove);
316
317
318 sessionCache.entrySet()
319 .stream()
320 .sorted((e1, e2) -> e1.getValue().getLastAccessedAt().compareTo(e2.getValue().getLastAccessedAt()))
321 .limit(toRemove)
322 .map(Map.Entry::getKey)
323 .collect(Collectors.toList())
324 .forEach(sessionCache::remove);
325 }
326 }
327
328
329
330
331
332
333
334 protected boolean isExpired(final ChatSession session) {
335 final int timeoutMinutes = getSessionTimeoutMinutes();
336 final LocalDateTime expirationTime = session.getLastAccessedAt().plusMinutes(timeoutMinutes);
337 return LocalDateTime.now().isAfter(expirationTime);
338 }
339
340
341
342
343
344
345 protected int getSessionTimeoutMinutes() {
346 final int value = ComponentUtil.getFessConfig().getRagChatSessionTimeoutMinutesAsInteger();
347 if (value <= 0) {
348 logger.warn("Invalid session timeout: {}. Using default: 30", value);
349 return 30;
350 }
351 return value;
352 }
353
354
355
356
357
358
359 protected int getMaxSessionSize() {
360 final int value = ComponentUtil.getFessConfig().getRagChatSessionMaxSizeAsInteger();
361 if (value <= 0) {
362 logger.warn("Invalid max session size: {}. Using default: 100", value);
363 return 100;
364 }
365 return value;
366 }
367
368
369
370
371
372
373 protected int getMaxHistoryMessages() {
374 final int value = ComponentUtil.getFessConfig().getRagChatHistoryMaxMessagesAsInteger();
375 if (value <= 0) {
376 logger.warn("Invalid max history messages: {}. Using default: 20", value);
377 return 20;
378 }
379 return value;
380 }
381
382
383
384
385
386
387 public int getActiveSessionCount() {
388 return sessionCache.size();
389 }
390 }