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}