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.jakarta;
018
019import jakarta.servlet.ServletContext;
020import jakarta.servlet.http.HttpSession;
021import jakarta.servlet.http.HttpSessionBindingEvent;
022import jakarta.servlet.http.HttpSessionBindingListener;
023
024import javax.annotation.Nonnull;
025import javax.annotation.Nullable;
026import javax.annotation.concurrent.NotThreadSafe;
027import java.time.Instant;
028import java.util.Collections;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.Map;
033import java.util.Set;
034import java.util.UUID;
035
036import static java.util.Objects.requireNonNull;
037
038/**
039 * Soklet integration implementation of {@link HttpSession}.
040 *
041 * @author <a href="https://www.revetkn.com">Mark Allen</a>
042 */
043@NotThreadSafe
044public final class SokletHttpSession implements HttpSession {
045        @Nonnull
046        private UUID sessionId;
047        @Nonnull
048        private final Instant createdAt;
049        @Nonnull
050        private final Map<String, Object> attributes;
051        @Nonnull
052        private final ServletContext servletContext;
053        private boolean invalidated;
054        private int maxInactiveInterval;
055
056        @Nonnull
057        public static SokletHttpSession withServletContext(@Nonnull ServletContext servletContext) {
058                requireNonNull(servletContext);
059                return new SokletHttpSession(servletContext);
060        }
061
062        private 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        @Nullable
144        public Object getAttribute(@Nullable String name) {
145                ensureNotInvalidated();
146                return getAttributes().get(name);
147        }
148
149        @Override
150        @Nonnull
151        public Enumeration<String> getAttributeNames() {
152                ensureNotInvalidated();
153                return Collections.enumeration(getAttributes().keySet());
154        }
155
156        @Override
157        public void setAttribute(@Nonnull String name,
158                                                                                                         @Nullable Object value) {
159                requireNonNull(name);
160
161                ensureNotInvalidated();
162
163                if (value == null) {
164                        removeAttribute(name);
165                } else {
166                        Object existingValue = getAttributes().get(name);
167
168                        if (existingValue != null && existingValue instanceof HttpSessionBindingListener)
169                                ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue));
170
171                        getAttributes().put(name, value);
172
173                        if (value instanceof HttpSessionBindingListener)
174                                ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value));
175                }
176        }
177
178        @Override
179        public void removeAttribute(@Nonnull String name) {
180                requireNonNull(name);
181
182                ensureNotInvalidated();
183
184                Object existingValue = getAttributes().get(name);
185
186                if (existingValue != null && existingValue instanceof HttpSessionBindingListener)
187                        ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue));
188
189                getAttributes().remove(name);
190        }
191
192        @Override
193        public void invalidate() {
194                // Copy to prevent modification while iterating
195                Set<String> namesToRemove = new HashSet<>(getAttributes().keySet());
196
197                for (String name : namesToRemove)
198                        removeAttribute(name);
199
200                setInvalidated(true);
201        }
202
203        @Override
204        public boolean isNew() {
205                return true;
206        }
207}