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.jakarta; 018 019import jakarta.servlet.ServletContext; 020import jakarta.servlet.http.HttpSession; 021import jakarta.servlet.http.HttpSessionBindingEvent; 022import jakarta.servlet.http.HttpSessionBindingListener; 023import org.jspecify.annotations.NonNull; 024import org.jspecify.annotations.Nullable; 025 026import javax.annotation.concurrent.ThreadSafe; 027import java.time.Instant; 028import java.util.Collections; 029import java.util.Enumeration; 030import java.util.HashSet; 031import java.util.Map; 032import java.util.Set; 033import java.util.UUID; 034import java.util.concurrent.ConcurrentHashMap; 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@ThreadSafe 044public final class SokletHttpSession implements HttpSession { 045 @NonNull 046 private volatile UUID sessionId; 047 @NonNull 048 private final Instant createdAt; 049 @NonNull 050 private volatile Instant lastAccessedAt; 051 @NonNull 052 private final Map<@NonNull String, @NonNull Object> attributes; 053 @NonNull 054 private final ServletContext servletContext; 055 private volatile boolean invalidated; 056 private volatile int maxInactiveInterval; 057 private volatile boolean isNew; 058 059 @NonNull 060 public static SokletHttpSession fromServletContext(@NonNull ServletContext servletContext) { 061 requireNonNull(servletContext); 062 return new SokletHttpSession(servletContext); 063 } 064 065 private SokletHttpSession(@NonNull ServletContext servletContext) { 066 requireNonNull(servletContext); 067 068 this.sessionId = UUID.randomUUID(); 069 this.createdAt = Instant.now(); 070 this.lastAccessedAt = this.createdAt; 071 this.attributes = new ConcurrentHashMap<>(); 072 this.servletContext = servletContext; 073 this.invalidated = false; 074 this.maxInactiveInterval = 0; 075 this.isNew = true; 076 } 077 078 public void setSessionId(@NonNull UUID sessionId) { 079 requireNonNull(sessionId); 080 this.sessionId = sessionId; 081 } 082 083 @NonNull 084 private UUID getSessionId() { 085 return this.sessionId; 086 } 087 088 @NonNull 089 private Instant getCreatedAt() { 090 return this.createdAt; 091 } 092 093 @NonNull 094 private Instant getLastAccessedAt() { 095 return this.lastAccessedAt; 096 } 097 098 @NonNull 099 private Map<@NonNull String, @NonNull Object> getAttributes() { 100 return this.attributes; 101 } 102 103 boolean isInvalidated() { 104 return this.invalidated; 105 } 106 107 private void setInvalidated(boolean invalidated) { 108 this.invalidated = invalidated; 109 } 110 111 private void ensureNotInvalidated() { 112 if (isInvalidated()) 113 throw new IllegalStateException("Session is invalidated"); 114 } 115 116 void markAccessed() { 117 this.lastAccessedAt = Instant.now(); 118 } 119 120 void markNotNew() { 121 this.isNew = false; 122 } 123 124 // Implementation of HttpSession methods below: 125 126 @Override 127 public long getCreationTime() { 128 ensureNotInvalidated(); 129 return getCreatedAt().toEpochMilli(); 130 } 131 132 @Override 133 @NonNull 134 public String getId() { 135 ensureNotInvalidated(); 136 return getSessionId().toString(); 137 } 138 139 @Override 140 public long getLastAccessedTime() { 141 ensureNotInvalidated(); 142 return getLastAccessedAt().toEpochMilli(); 143 } 144 145 @Override 146 @NonNull 147 public ServletContext getServletContext() { 148 ensureNotInvalidated(); 149 return this.servletContext; 150 } 151 152 @Override 153 public void setMaxInactiveInterval(int interval) { 154 ensureNotInvalidated(); 155 this.maxInactiveInterval = interval; 156 } 157 158 @Override 159 public int getMaxInactiveInterval() { 160 ensureNotInvalidated(); 161 return this.maxInactiveInterval; 162 } 163 164 @Override 165 @Nullable 166 public Object getAttribute(@Nullable String name) { 167 ensureNotInvalidated(); 168 return getAttributes().get(name); 169 } 170 171 @Override 172 @NonNull 173 public Enumeration<@NonNull String> getAttributeNames() { 174 ensureNotInvalidated(); 175 return Collections.enumeration(getAttributes().keySet()); 176 } 177 178 @Override 179 public void setAttribute(@NonNull String name, 180 @Nullable Object value) { 181 requireNonNull(name); 182 183 ensureNotInvalidated(); 184 185 if (value == null) { 186 removeAttribute(name); 187 } else { 188 Object existingValue = getAttributes().get(name); 189 190 if (existingValue != null && existingValue instanceof HttpSessionBindingListener) 191 ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue)); 192 193 getAttributes().put(name, value); 194 195 if (value instanceof HttpSessionBindingListener) 196 ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value)); 197 } 198 } 199 200 @Override 201 public void removeAttribute(@NonNull String name) { 202 requireNonNull(name); 203 204 ensureNotInvalidated(); 205 206 Object existingValue = getAttributes().get(name); 207 208 if (existingValue != null && existingValue instanceof HttpSessionBindingListener) 209 ((HttpSessionBindingListener) existingValue).valueUnbound(new HttpSessionBindingEvent(this, name, existingValue)); 210 211 getAttributes().remove(name); 212 } 213 214 @Override 215 public void invalidate() { 216 ensureNotInvalidated(); 217 // Copy to prevent modification while iterating 218 Set<@NonNull String> namesToRemove = new HashSet<>(getAttributes().keySet()); 219 220 for (String name : namesToRemove) 221 removeAttribute(name); 222 223 setInvalidated(true); 224 } 225 226 @Override 227 public boolean isNew() { 228 ensureNotInvalidated(); 229 return this.isNew; 230 } 231}