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}