Skip to content

Commit 48c72a2

Browse files
committed
refactor(auth): replace reflection with interface+registry pattern for session store
- Add QSessionStoreProviderInterface in core for providers to implement - Add QSessionStoreRegistry singleton for providers to register on startup - Refactor QSessionStoreHelper to use registry instead of reflection - Add loadAndTouchSession() for combined load+touch operation - Update OAuth2AuthenticationModule to use loadAndTouchSession()
1 parent 29acb1f commit 48c72a2

File tree

5 files changed

+459
-93
lines changed

5 files changed

+459
-93
lines changed

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QSessionStoreHelper.java

Lines changed: 60 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
package com.kingsrook.qqq.backend.core.modules.authentication;
2323

2424

25-
import java.lang.reflect.Method;
2625
import java.time.Duration;
2726
import java.util.Optional;
2827
import com.kingsrook.qqq.backend.core.logging.QLogger;
@@ -31,63 +30,45 @@
3130

3231

3332
/*******************************************************************************
34-
** Helper class for optionally interacting with the QSessionStore QBit.
33+
** Helper class for interacting with the session store.
3534
**
36-
** Uses reflection to avoid a hard dependency on the qbit-session-store module.
37-
** All methods are designed to fail silently (returning empty/default values)
38-
** when the QBit is not on the classpath, ensuring backwards compatibility.
35+
** Delegates to the provider registered with QSessionStoreRegistry. All methods
36+
** are designed to fail silently (returning empty/default values) when no
37+
** provider is registered, ensuring backwards compatibility.
3938
*******************************************************************************/
4039
public class QSessionStoreHelper
4140
{
4241
private static final QLogger LOG = QLogger.getLogger(QSessionStoreHelper.class);
4342

44-
private static final String CONTEXT_CLASS = "com.kingsrook.qbits.sessionstore.QSessionStoreQBitContext";
45-
46-
private static Boolean sessionStoreAvailable = null;
43+
private static final Duration DEFAULT_TTL = Duration.ofHours(1);
4744

4845

4946

5047
/***************************************************************************
51-
** Check if the session store QBit is available on the classpath.
48+
** Check if a session store provider is available.
5249
***************************************************************************/
5350
public static boolean isSessionStoreAvailable()
5451
{
55-
if(sessionStoreAvailable == null)
56-
{
57-
try
58-
{
59-
Class.forName(CONTEXT_CLASS);
60-
sessionStoreAvailable = true;
61-
}
62-
catch(ClassNotFoundException e)
63-
{
64-
sessionStoreAvailable = false;
65-
}
66-
}
67-
return sessionStoreAvailable;
52+
return QSessionStoreRegistry.getInstance().isAvailable();
6853
}
6954

7055

7156

7257
/***************************************************************************
73-
** Store a session in the session store (if available and configured).
58+
** Store a session in the session store (if available).
7459
***************************************************************************/
7560
public static void storeSession(String sessionUuid, QSession session, Duration ttl)
7661
{
77-
if(!isSessionStoreAvailable())
62+
Optional<QSessionStoreProviderInterface> provider = QSessionStoreRegistry.getInstance().getProvider();
63+
if(provider.isEmpty())
7864
{
7965
return;
8066
}
8167

8268
try
8369
{
84-
Object provider = getProvider();
85-
if(provider != null)
86-
{
87-
Method storeMethod = provider.getClass().getMethod("store", String.class, QSession.class, Duration.class);
88-
storeMethod.invoke(provider, sessionUuid, session, ttl);
89-
LOG.debug("Stored session in session store", logPair("sessionUuid", sessionUuid));
90-
}
70+
provider.get().store(sessionUuid, session, ttl);
71+
LOG.debug("Stored session in session store", logPair("sessionUuid", sessionUuid));
9172
}
9273
catch(Exception e)
9374
{
@@ -98,29 +79,24 @@ public static void storeSession(String sessionUuid, QSession session, Duration t
9879

9980

10081
/***************************************************************************
101-
** Load a session from the session store (if available and configured).
82+
** Load a session from the session store (if available).
10283
***************************************************************************/
103-
@SuppressWarnings("unchecked")
10484
public static Optional<QSession> loadSession(String sessionUuid)
10585
{
106-
if(!isSessionStoreAvailable())
86+
Optional<QSessionStoreProviderInterface> provider = QSessionStoreRegistry.getInstance().getProvider();
87+
if(provider.isEmpty())
10788
{
10889
return Optional.empty();
10990
}
11091

11192
try
11293
{
113-
Object provider = getProvider();
114-
if(provider != null)
94+
Optional<QSession> result = provider.get().load(sessionUuid);
95+
if(result.isPresent())
11596
{
116-
Method loadMethod = provider.getClass().getMethod("load", String.class);
117-
Optional<QSession> result = (Optional<QSession>) loadMethod.invoke(provider, sessionUuid);
118-
if(result.isPresent())
119-
{
120-
LOG.debug("Loaded session from session store", logPair("sessionUuid", sessionUuid));
121-
}
122-
return result;
97+
LOG.debug("Loaded session from session store", logPair("sessionUuid", sessionUuid));
12398
}
99+
return result;
124100
}
125101
catch(Exception e)
126102
{
@@ -133,72 +109,83 @@ public static Optional<QSession> loadSession(String sessionUuid)
133109

134110

135111
/***************************************************************************
136-
** Touch a session to reset its TTL (if available and configured).
112+
** Load a session and touch it to reset its TTL in a single operation.
113+
**
114+
** This is more efficient than calling loadSession + touchSession separately,
115+
** as providers may implement optimized single-call versions (e.g., Redis
116+
** GETEX, combined SQL query).
137117
***************************************************************************/
138-
public static void touchSession(String sessionUuid)
118+
public static Optional<QSession> loadAndTouchSession(String sessionUuid)
139119
{
140-
if(!isSessionStoreAvailable())
120+
Optional<QSessionStoreProviderInterface> provider = QSessionStoreRegistry.getInstance().getProvider();
121+
if(provider.isEmpty())
141122
{
142-
return;
123+
return Optional.empty();
143124
}
144125

145126
try
146127
{
147-
Object provider = getProvider();
148-
if(provider != null)
128+
Optional<QSession> result = provider.get().loadAndTouch(sessionUuid);
129+
if(result.isPresent())
149130
{
150-
Method touchMethod = provider.getClass().getMethod("touch", String.class);
151-
touchMethod.invoke(provider, sessionUuid);
131+
LOG.debug("Loaded and touched session from session store", logPair("sessionUuid", sessionUuid));
152132
}
133+
return result;
153134
}
154135
catch(Exception e)
155136
{
156-
LOG.warn("Failed to touch session in session store", e, logPair("sessionUuid", sessionUuid));
137+
LOG.warn("Failed to load and touch session in session store", e, logPair("sessionUuid", sessionUuid));
157138
}
139+
140+
return Optional.empty();
158141
}
159142

160143

161144

162145
/***************************************************************************
163-
** Get the configured default TTL from the session store config.
146+
** Touch a session to reset its TTL (if available).
164147
***************************************************************************/
165-
public static Duration getDefaultTtl()
148+
public static void touchSession(String sessionUuid)
166149
{
167-
if(!isSessionStoreAvailable())
150+
Optional<QSessionStoreProviderInterface> provider = QSessionStoreRegistry.getInstance().getProvider();
151+
if(provider.isEmpty())
168152
{
169-
return Duration.ofHours(1);
153+
return;
170154
}
171155

172156
try
173157
{
174-
Class<?> contextClass = Class.forName(CONTEXT_CLASS);
175-
Method getConfigMethod = contextClass.getMethod("getConfig");
176-
Object config = getConfigMethod.invoke(null);
177-
178-
if(config != null)
179-
{
180-
Method getTtlMethod = config.getClass().getMethod("getDefaultTtl");
181-
return (Duration) getTtlMethod.invoke(config);
182-
}
158+
provider.get().touch(sessionUuid);
183159
}
184160
catch(Exception e)
185161
{
186-
LOG.debug("Failed to get default TTL from session store config", e);
162+
LOG.warn("Failed to touch session in session store", e, logPair("sessionUuid", sessionUuid));
187163
}
188-
189-
return Duration.ofHours(1);
190164
}
191165

192166

193167

194168
/***************************************************************************
195-
** Get the provider instance from the context class.
169+
** Get the configured default TTL from the session store provider.
196170
***************************************************************************/
197-
private static Object getProvider() throws Exception
171+
public static Duration getDefaultTtl()
198172
{
199-
Class<?> contextClass = Class.forName(CONTEXT_CLASS);
200-
Method getProviderMethod = contextClass.getMethod("getProvider");
201-
return getProviderMethod.invoke(null);
173+
Optional<QSessionStoreProviderInterface> provider = QSessionStoreRegistry.getInstance().getProvider();
174+
if(provider.isEmpty())
175+
{
176+
return DEFAULT_TTL;
177+
}
178+
179+
try
180+
{
181+
return provider.get().getDefaultTtl();
182+
}
183+
catch(Exception e)
184+
{
185+
LOG.debug("Failed to get default TTL from session store provider", e);
186+
}
187+
188+
return DEFAULT_TTL;
202189
}
203190

204191
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* QQQ - Low-code Application Framework for Engineers.
3+
* Copyright (C) 2021-2025. Kingsrook, LLC
4+
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
5+
* contact@kingsrook.com
6+
* https://github.com/Kingsrook/
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
22+
package com.kingsrook.qqq.backend.core.modules.authentication;
23+
24+
25+
import java.time.Duration;
26+
import java.util.Optional;
27+
import com.kingsrook.qqq.backend.core.model.session.QSession;
28+
29+
30+
/*******************************************************************************
31+
** Interface for session storage providers.
32+
**
33+
** QBits or application code can implement this interface to provide session
34+
** persistence. Implementations register themselves with QSessionStoreRegistry
35+
** on startup, and QQQ core uses the registered provider for session caching.
36+
**
37+
** Example providers:
38+
** - InMemory: ConcurrentHashMap for dev/testing
39+
** - TableBased: QQQ table storage for multi-instance persistence
40+
** - Redis: Distributed caching for HA deployments
41+
*******************************************************************************/
42+
public interface QSessionStoreProviderInterface
43+
{
44+
45+
/***************************************************************************
46+
** Store a session with the given TTL.
47+
**
48+
** @param sessionUuid Unique identifier for the session
49+
** @param session The session to store
50+
** @param ttl Time-to-live for the session
51+
***************************************************************************/
52+
void store(String sessionUuid, QSession session, Duration ttl);
53+
54+
55+
/***************************************************************************
56+
** Load a session by UUID.
57+
**
58+
** @param sessionUuid Unique identifier for the session
59+
** @return Optional containing the session if found and not expired
60+
***************************************************************************/
61+
Optional<QSession> load(String sessionUuid);
62+
63+
64+
/***************************************************************************
65+
** Remove a session by UUID.
66+
**
67+
** @param sessionUuid Unique identifier for the session to remove
68+
***************************************************************************/
69+
void remove(String sessionUuid);
70+
71+
72+
/***************************************************************************
73+
** Touch a session to reset its TTL (sliding expiration).
74+
**
75+
** @param sessionUuid Unique identifier for the session to touch
76+
***************************************************************************/
77+
void touch(String sessionUuid);
78+
79+
80+
/***************************************************************************
81+
** Get the default TTL for sessions.
82+
**
83+
** @return The default time-to-live duration
84+
***************************************************************************/
85+
Duration getDefaultTtl();
86+
87+
88+
/***************************************************************************
89+
** Load a session and touch it to reset its TTL in a single operation.
90+
**
91+
** Default implementation calls load() then touch(). Providers may override
92+
** with optimized implementations (e.g., Redis GETEX, combined SQL query).
93+
**
94+
** @param sessionUuid Unique identifier for the session
95+
** @return Optional containing the session if found and not expired
96+
***************************************************************************/
97+
default Optional<QSession> loadAndTouch(String sessionUuid)
98+
{
99+
Optional<QSession> session = load(sessionUuid);
100+
session.ifPresent(s -> touch(sessionUuid));
101+
return session;
102+
}
103+
104+
}

0 commit comments

Comments
 (0)