001/*
002 * Copyright 2024-2025 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 javax.annotation.Nonnull;
020import javax.annotation.Nullable;
021import javax.annotation.concurrent.NotThreadSafe;
022import javax.servlet.ServletContext;
023import javax.servlet.http.HttpSession;
024import javax.servlet.http.HttpSessionBindingEvent;
025import javax.servlet.http.HttpSessionBindingListener;
026import javax.servlet.http.HttpSessionContext;
027import java.time.Instant;
028import java.util.Collections;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.UUID;
036
037import static java.util.Objects.requireNonNull;
038
039/**
040 * Soklet integration implementation of {@link HttpSession}.
041 *
042 * @author <a href="https://www.revetkn.com">Mark Allen</a>
043 */
044@NotThreadSafe
045public final class SokletHttpSession implements HttpSession {
046        @Nonnull
047        private static final HttpSessionContext SHARED_HTTP_SESSION_CONTEXT;
048
049        static {
050                SHARED_HTTP_SESSION_CONTEXT = SokletHttpSessionContext.withDefaults();
051        }
052
053        @Nonnull
054        private UUID sessionId;
055        @Nonnull
056        private final Instant createdAt;
057        @Nonnull
058        private final Map<String, Object> attributes;
059        @Nonnull
060        private final ServletContext servletContext;
061        private boolean invalidated;
062        private int maxInactiveInterval;
063
064        @Nonnull
065        public static SokletHttpSession withServletContext(@Nonnull ServletContext servletContext) {
066                requireNonNull(servletContext);
067                return new SokletHttpSession(servletContext);
068        }
069
070        private SokletHttpSession(@Nonnull ServletContext servletContext) {
071                requireNonNull(servletContext);
072
073                this.sessionId = UUID.randomUUID();
074                this.createdAt = Instant.now();
075                this.attributes = new HashMap<>();
076                this.servletContext = servletContext;
077                this.invalidated = false;
078                this.maxInactiveInterval = 0;
079        }
080
081        public void setSessionId(@Nonnull UUID sessionId) {
082                requireNonNull(sessionId);
083                this.sessionId = sessionId;
084        }
085
086        @Nonnull
087        protected UUID getSessionId() {
088                return this.sessionId;
089        }
090
091        @Nonnull
092        protected Instant getCreatedAt() {
093                return this.createdAt;
094        }
095
096        @Nonnull
097        protected Map<String, Object> getAttributes() {
098                return this.attributes;
099        }
100
101        protected boolean isInvalidated() {
102                return this.invalidated;
103        }
104
105        protected void setInvalidated(boolean invalidated) {
106                this.invalidated = invalidated;
107        }
108
109        protected void ensureNotInvalidated() {
110                if (isInvalidated())
111                        throw new IllegalStateException("Session is invalidated");
112        }
113
114        // Implementation of HttpSession methods below:
115
116        @Override
117        public long getCreationTime() {
118                ensureNotInvalidated();
119                return getCreatedAt().toEpochMilli();
120        }
121
122        @Override
123        @Nonnull
124        public String getId() {
125                return getSessionId().toString();
126        }
127
128        @Override
129        public long getLastAccessedTime() {
130                ensureNotInvalidated();
131                return getCreatedAt().toEpochMilli();
132        }
133
134        @Override
135        @Nonnull
136        public ServletContext getServletContext() {
137                return this.servletContext;
138        }
139
140        @Override
141        public void setMaxInactiveInterval(int interval) {
142                this.maxInactiveInterval = interval;
143        }
144
145        @Override
146        public int getMaxInactiveInterval() {
147                return this.maxInactiveInterval;
148        }
149
150        @Override
151        @Nonnull
152        @Deprecated
153        public HttpSessionContext getSessionContext() {
154                return SHARED_HTTP_SESSION_CONTEXT;
155        }
156
157        @Override
158        @Nullable
159        public Object getAttribute(@Nullable String name) {
160                ensureNotInvalidated();
161                return getAttributes().get(name);
162        }
163
164        @Override
165        @Nullable
166        @Deprecated
167        public Object getValue(@Nullable String name) {
168                ensureNotInvalidated();
169                return getAttribute(name);
170        }
171
172        @Override
173        @Nonnull
174        public Enumeration<String> getAttributeNames() {
175                ensureNotInvalidated();
176                return Collections.enumeration(getAttributes().keySet());
177        }
178
179        @Override
180        @Nonnull
181        @Deprecated
182        public String[] getValueNames() {
183                ensureNotInvalidated();
184                List<String> valueNames = Collections.list(getAttributeNames());
185                return valueNames.toArray(new String[0]);
186        }
187
188        @Override
189        public void setAttribute(@Nonnull String name,
190                                                                                                         @Nullable Object value) {
191                requireNonNull(name);
192
193                ensureNotInvalidated();
194
195                if (value == null) {
196                        removeAttribute(name);
197                } else {
198                        Object existingValue = getAttributes().get(name);
199
200                        if (existingValue != null && existingValue instanceof HttpSessionBindingListener)
201                                ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue));
202
203                        getAttributes().put(name, value);
204
205                        if (value instanceof HttpSessionBindingListener)
206                                ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value));
207                }
208        }
209
210        @Override
211        @Deprecated
212        public void putValue(@Nonnull String name,
213                                                                                         @Nonnull Object value) {
214                requireNonNull(name);
215                requireNonNull(value);
216
217                ensureNotInvalidated();
218                setAttribute(name, value);
219        }
220
221        @Override
222        public void removeAttribute(@Nonnull String name) {
223                requireNonNull(name);
224
225                ensureNotInvalidated();
226
227                Object existingValue = getAttributes().get(name);
228
229                if (existingValue != null && existingValue instanceof HttpSessionBindingListener)
230                        ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue));
231
232                getAttributes().remove(name);
233        }
234
235        @Override
236        @Deprecated
237        public void removeValue(@Nonnull String name) {
238                requireNonNull(name);
239
240                ensureNotInvalidated();
241                removeAttribute(name);
242        }
243
244        @Override
245        public void invalidate() {
246                // Copy to prevent modification while iterating
247                Set<String> namesToRemove = new HashSet<>(getAttributes().keySet());
248
249                for (String name : namesToRemove)
250                        removeAttribute(name);
251
252                setInvalidated(true);
253        }
254
255        @Override
256        public boolean isNew() {
257                return true;
258        }
259}