001/* 002 * Copyright 2002-2018 the original author or authors. 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 * https://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 org.springframework.messaging.simp; 018 019import java.util.Map; 020 021import org.apache.commons.logging.Log; 022 023import org.springframework.lang.Nullable; 024import org.springframework.messaging.Message; 025import org.springframework.messaging.MessageHeaders; 026import org.springframework.util.Assert; 027import org.springframework.util.StringUtils; 028 029/** 030 * A wrapper class for access to attributes associated with a SiMP session 031 * (e.g. WebSocket session). 032 * 033 * @author Rossen Stoyanchev 034 * @since 4.1 035 */ 036public class SimpAttributes { 037 038 /** Key for the mutex session attribute. */ 039 public static final String SESSION_MUTEX_NAME = SimpAttributes.class.getName() + ".MUTEX"; 040 041 /** Key set after the session is completed. */ 042 public static final String SESSION_COMPLETED_NAME = SimpAttributes.class.getName() + ".COMPLETED"; 043 044 /** Prefix for the name of session attributes used to store destruction callbacks. */ 045 public static final String DESTRUCTION_CALLBACK_NAME_PREFIX = 046 SimpAttributes.class.getName() + ".DESTRUCTION_CALLBACK."; 047 048 private static final Log logger = SimpLogging.forLogName(SimpAttributes.class); 049 050 051 private final String sessionId; 052 053 private final Map<String, Object> attributes; 054 055 056 /** 057 * Constructor wrapping the given session attributes map. 058 * @param sessionId the id of the associated session 059 * @param attributes the attributes 060 */ 061 public SimpAttributes(String sessionId, Map<String, Object> attributes) { 062 Assert.notNull(sessionId, "'sessionId' is required"); 063 Assert.notNull(attributes, "'attributes' is required"); 064 this.sessionId = sessionId; 065 this.attributes = attributes; 066 } 067 068 069 /** 070 * Return the value for the attribute of the given name, if any. 071 * @param name the name of the attribute 072 * @return the current attribute value, or {@code null} if not found 073 */ 074 @Nullable 075 public Object getAttribute(String name) { 076 return this.attributes.get(name); 077 } 078 079 /** 080 * Set the value with the given name replacing an existing value (if any). 081 * @param name the name of the attribute 082 * @param value the value for the attribute 083 */ 084 public void setAttribute(String name, Object value) { 085 this.attributes.put(name, value); 086 } 087 088 /** 089 * Remove the attribute of the given name, if it exists. 090 * <p>Also removes the registered destruction callback for the specified 091 * attribute, if any. However it <i>does not</i> execute the callback. 092 * It is assumed the removed object will continue to be used and destroyed 093 * independently at the appropriate time. 094 * @param name the name of the attribute 095 */ 096 public void removeAttribute(String name) { 097 this.attributes.remove(name); 098 removeDestructionCallback(name); 099 } 100 101 /** 102 * Retrieve the names of all attributes. 103 * @return the attribute names as String array, never {@code null} 104 */ 105 public String[] getAttributeNames() { 106 return StringUtils.toStringArray(this.attributes.keySet()); 107 } 108 109 /** 110 * Register a callback to execute on destruction of the specified attribute. 111 * The callback is executed when the session is closed. 112 * @param name the name of the attribute to register the callback for 113 * @param callback the destruction callback to be executed 114 */ 115 public void registerDestructionCallback(String name, Runnable callback) { 116 synchronized (getSessionMutex()) { 117 if (isSessionCompleted()) { 118 throw new IllegalStateException("Session id=" + getSessionId() + " already completed"); 119 } 120 this.attributes.put(DESTRUCTION_CALLBACK_NAME_PREFIX + name, callback); 121 } 122 } 123 124 private void removeDestructionCallback(String name) { 125 synchronized (getSessionMutex()) { 126 this.attributes.remove(DESTRUCTION_CALLBACK_NAME_PREFIX + name); 127 } 128 } 129 130 /** 131 * Return an id for the associated session. 132 * @return the session id as String (never {@code null}) 133 */ 134 public String getSessionId() { 135 return this.sessionId; 136 } 137 138 /** 139 * Expose the object to synchronize on for the underlying session. 140 * @return the session mutex to use (never {@code null}) 141 */ 142 public Object getSessionMutex() { 143 Object mutex = this.attributes.get(SESSION_MUTEX_NAME); 144 if (mutex == null) { 145 mutex = this.attributes; 146 } 147 return mutex; 148 } 149 150 /** 151 * Whether the {@link #sessionCompleted()} was already invoked. 152 */ 153 public boolean isSessionCompleted() { 154 return (this.attributes.get(SESSION_COMPLETED_NAME) != null); 155 } 156 157 /** 158 * Invoked when the session is completed. Executed completion callbacks. 159 */ 160 public void sessionCompleted() { 161 synchronized (getSessionMutex()) { 162 if (!isSessionCompleted()) { 163 executeDestructionCallbacks(); 164 this.attributes.put(SESSION_COMPLETED_NAME, Boolean.TRUE); 165 } 166 } 167 } 168 169 private void executeDestructionCallbacks() { 170 this.attributes.forEach((key, value) -> { 171 if (key.startsWith(DESTRUCTION_CALLBACK_NAME_PREFIX)) { 172 try { 173 ((Runnable) value).run(); 174 } 175 catch (Throwable ex) { 176 logger.error("Uncaught error in session attribute destruction callback", ex); 177 } 178 } 179 }); 180 } 181 182 183 /** 184 * Extract the SiMP session attributes from the given message and 185 * wrap them in a {@link SimpAttributes} instance. 186 * @param message the message to extract session attributes from 187 */ 188 public static SimpAttributes fromMessage(Message<?> message) { 189 Assert.notNull(message, "Message must not be null"); 190 MessageHeaders headers = message.getHeaders(); 191 String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); 192 Map<String, Object> sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(headers); 193 if (sessionId == null) { 194 throw new IllegalStateException("No session id in " + message); 195 } 196 if (sessionAttributes == null) { 197 throw new IllegalStateException("No session attributes in " + message); 198 } 199 return new SimpAttributes(sessionId, sessionAttributes); 200 } 201 202}