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