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}