001/*
002 * Copyright 2024-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet.servlet.javax;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.ThreadSafe;
023import javax.servlet.ServletContext;
024import javax.servlet.http.HttpSession;
025import javax.servlet.http.HttpSessionBindingEvent;
026import javax.servlet.http.HttpSessionBindingListener;
027import javax.servlet.http.HttpSessionContext;
028import java.time.Instant;
029import java.util.Collections;
030import java.util.Enumeration;
031import java.util.HashSet;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.UUID;
036import java.util.concurrent.ConcurrentHashMap;
037
038import static java.util.Objects.requireNonNull;
039
040/**
041 * Soklet integration implementation of {@link HttpSession}.
042 *
043 * @author <a href="https://www.revetkn.com">Mark Allen</a>
044 */
045@ThreadSafe
046public final class SokletHttpSession implements HttpSession {
047        @NonNull
048        private static final HttpSessionContext SHARED_HTTP_SESSION_CONTEXT;
049
050        static {
051                SHARED_HTTP_SESSION_CONTEXT = SokletHttpSessionContext.fromDefaults();
052        }
053
054        @NonNull
055        private volatile UUID sessionId;
056        @NonNull
057        private final Instant createdAt;
058        @NonNull
059        private volatile Instant lastAccessedAt;
060        @NonNull
061        private final Map<@NonNull String, @NonNull Object> attributes;
062        @NonNull
063        private final ServletContext servletContext;
064        private volatile boolean invalidated;
065        private volatile int maxInactiveInterval;
066        private volatile boolean isNew;
067
068        @NonNull
069        public static SokletHttpSession fromServletContext(@NonNull ServletContext servletContext) {
070                requireNonNull(servletContext);
071                return new SokletHttpSession(servletContext);
072        }
073
074        private SokletHttpSession(@NonNull ServletContext servletContext) {
075                requireNonNull(servletContext);
076
077                this.sessionId = UUID.randomUUID();
078                this.createdAt = Instant.now();
079                this.lastAccessedAt = this.createdAt;
080                this.attributes = new ConcurrentHashMap<>();
081                this.servletContext = servletContext;
082                this.invalidated = false;
083                this.maxInactiveInterval = 0;
084                this.isNew = true;
085        }
086
087        public void setSessionId(@NonNull UUID sessionId) {
088                requireNonNull(sessionId);
089                this.sessionId = sessionId;
090        }
091
092        @NonNull
093        private UUID getSessionId() {
094                return this.sessionId;
095        }
096
097        @NonNull
098        private Instant getCreatedAt() {
099                return this.createdAt;
100        }
101
102        @NonNull
103        private Instant getLastAccessedAt() {
104                return this.lastAccessedAt;
105        }
106
107        @NonNull
108        private Map<@NonNull String, @NonNull Object> getAttributes() {
109                return this.attributes;
110        }
111
112        boolean isInvalidated() {
113                return this.invalidated;
114        }
115
116        private void setInvalidated(boolean invalidated) {
117                this.invalidated = invalidated;
118        }
119
120        private void ensureNotInvalidated() {
121                if (isInvalidated())
122                        throw new IllegalStateException("Session is invalidated");
123        }
124
125        void markAccessed() {
126                this.lastAccessedAt = Instant.now();
127        }
128
129        void markNotNew() {
130                this.isNew = false;
131        }
132
133        // Implementation of HttpSession methods below:
134
135        @Override
136        public long getCreationTime() {
137                ensureNotInvalidated();
138                return getCreatedAt().toEpochMilli();
139        }
140
141        @Override
142        @NonNull
143        public String getId() {
144                ensureNotInvalidated();
145                return getSessionId().toString();
146        }
147
148        @Override
149        public long getLastAccessedTime() {
150                ensureNotInvalidated();
151                return getLastAccessedAt().toEpochMilli();
152        }
153
154        @Override
155        @NonNull
156        public ServletContext getServletContext() {
157                ensureNotInvalidated();
158                return this.servletContext;
159        }
160
161        @Override
162        public void setMaxInactiveInterval(int interval) {
163                ensureNotInvalidated();
164                this.maxInactiveInterval = interval;
165        }
166
167        @Override
168        public int getMaxInactiveInterval() {
169                ensureNotInvalidated();
170                return this.maxInactiveInterval;
171        }
172
173        @Override
174        @NonNull
175        @Deprecated
176        public HttpSessionContext getSessionContext() {
177                ensureNotInvalidated();
178                return SHARED_HTTP_SESSION_CONTEXT;
179        }
180
181        @Override
182        @Nullable
183        public Object getAttribute(@Nullable String name) {
184                ensureNotInvalidated();
185                return getAttributes().get(name);
186        }
187
188        @Override
189        @Nullable
190        @Deprecated
191        public Object getValue(@Nullable String name) {
192                ensureNotInvalidated();
193                return getAttribute(name);
194        }
195
196        @Override
197        @NonNull
198        public Enumeration<@NonNull String> getAttributeNames() {
199                ensureNotInvalidated();
200                return Collections.enumeration(getAttributes().keySet());
201        }
202
203        @Override
204        @Deprecated
205        public @NonNull String @NonNull [] getValueNames() {
206                ensureNotInvalidated();
207                List<@NonNull String> valueNames = Collections.list(getAttributeNames());
208                return valueNames.toArray(new String[0]);
209        }
210
211        @Override
212        public void setAttribute(@NonNull String name,
213                                                                                                         @Nullable Object value) {
214                requireNonNull(name);
215
216                ensureNotInvalidated();
217
218                if (value == null) {
219                        removeAttribute(name);
220                } else {
221                        Object existingValue = getAttributes().get(name);
222
223                        if (existingValue != null && existingValue instanceof HttpSessionBindingListener)
224                                ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue));
225
226                        getAttributes().put(name, value);
227
228                        if (value instanceof HttpSessionBindingListener)
229                                ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value));
230                }
231        }
232
233        @Override
234        @Deprecated
235        public void putValue(@NonNull String name,
236                                                                                         @NonNull Object value) {
237                requireNonNull(name);
238                requireNonNull(value);
239
240                ensureNotInvalidated();
241                setAttribute(name, value);
242        }
243
244        @Override
245        public void removeAttribute(@NonNull String name) {
246                requireNonNull(name);
247
248                ensureNotInvalidated();
249
250                Object existingValue = getAttributes().get(name);
251
252                if (existingValue != null && existingValue instanceof HttpSessionBindingListener)
253                        ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue));
254
255                getAttributes().remove(name);
256        }
257
258        @Override
259        @Deprecated
260        public void removeValue(@NonNull String name) {
261                requireNonNull(name);
262
263                ensureNotInvalidated();
264                removeAttribute(name);
265        }
266
267        @Override
268        public void invalidate() {
269                ensureNotInvalidated();
270                // Copy to prevent modification while iterating
271                Set<@NonNull String> namesToRemove = new HashSet<>(getAttributes().keySet());
272
273                for (String name : namesToRemove)
274                        removeAttribute(name);
275
276                setInvalidated(true);
277        }
278
279        @Override
280        public boolean isNew() {
281                ensureNotInvalidated();
282                return this.isNew;
283        }
284}