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