1 /* 2 * Copyright 2002-2018 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 module hunt.stomp.simp.SimpAttributes; 18 19 import hunt.stomp.simp.SimpMessageHeaderAccessor; 20 import hunt.stomp.Message; 21 import hunt.stomp.MessageHeaders; 22 23 import hunt.collection.Map; 24 import hunt.logging; 25 import hunt.Boolean; 26 import hunt.util.Common; 27 import hunt.Exceptions; 28 import hunt.text.Common; 29 import hunt.text.StringUtils; 30 31 import std.string; 32 import std.traits; 33 34 35 /** 36 * A wrapper class for access to attributes associated with a SiMP session 37 * (e.g. WebSocket session). 38 * 39 * @author Rossen Stoyanchev 40 * @since 4.1 41 */ 42 class SimpAttributes { 43 44 /** Key for the mutex session attribute. */ 45 // SimpAttributes.class.getName() 46 enum string SESSION_MUTEX_NAME = fullyQualifiedName!SimpAttributes ~ ".MUTEX"; 47 48 /** Key set after the session is completed. */ 49 enum string SESSION_COMPLETED_NAME = fullyQualifiedName!SimpAttributes ~ ".COMPLETED"; 50 51 /** Prefix for the name of session attributes used to store destruction callbacks. */ 52 enum string DESTRUCTION_CALLBACK_NAME_PREFIX = 53 fullyQualifiedName!SimpAttributes ~ ".DESTRUCTION_CALLBACK."; 54 55 56 private string sessionId; 57 58 private Map!(string, Object) attributes; 59 60 61 /** 62 * Constructor wrapping the given session attributes map. 63 * @param sessionId the id of the associated session 64 * @param attributes the attributes 65 */ 66 this(string sessionId, Map!(string, Object) attributes) { 67 assert(sessionId, "'sessionId' is required"); 68 assert(attributes, "'attributes' is required"); 69 this.sessionId = sessionId; 70 this.attributes = attributes; 71 } 72 73 74 /** 75 * Return the value for the attribute of the given name, if any. 76 * @param name the name of the attribute 77 * @return the current attribute value, or {@code null} if not found 78 */ 79 80 Object getAttribute(string name) { 81 return this.attributes.get(name); 82 } 83 84 /** 85 * Set the value with the given name replacing an existing value (if any). 86 * @param name the name of the attribute 87 * @param value the value for the attribute 88 */ 89 void setAttribute(string name, Object value) { 90 this.attributes.put(name, value); 91 } 92 93 /** 94 * Remove the attribute of the given name, if it exists. 95 * <p>Also removes the registered destruction callback for the specified 96 * attribute, if any. However it <i>does not</i> execute the callback. 97 * It is assumed the removed object will continue to be used and destroyed 98 * independently at the appropriate time. 99 * @param name the name of the attribute 100 */ 101 void removeAttribute(string name) { 102 this.attributes.remove(name); 103 removeDestructionCallback(name); 104 } 105 106 /** 107 * Retrieve the names of all attributes. 108 * @return the attribute names as string array, never {@code null} 109 */ 110 string[] getAttributeNames() { 111 return StringUtils.toStringArray(this.attributes.byKey); 112 } 113 114 /** 115 * Register a callback to execute on destruction of the specified attribute. 116 * The callback is executed when the session is closed. 117 * @param name the name of the attribute to register the callback for 118 * @param callback the destruction callback to be executed 119 */ 120 void registerDestructionCallback(string name, Runnable callback) { 121 synchronized (getSessionMutex()) { 122 if (isSessionCompleted()) { 123 throw new IllegalStateException("Session id=" ~ getSessionId() ~ " already completed"); 124 } 125 this.attributes.put(DESTRUCTION_CALLBACK_NAME_PREFIX ~ name, cast(Object)callback); 126 } 127 } 128 129 private void removeDestructionCallback(string name) { 130 synchronized (getSessionMutex()) { 131 this.attributes.remove(DESTRUCTION_CALLBACK_NAME_PREFIX ~ name); 132 } 133 } 134 135 /** 136 * Return an id for the associated session. 137 * @return the session id as string (never {@code null}) 138 */ 139 string getSessionId() { 140 return this.sessionId; 141 } 142 143 /** 144 * Expose the object to synchronize on for the underlying session. 145 * @return the session mutex to use (never {@code null}) 146 */ 147 Object getSessionMutex() { 148 Object mutex = this.attributes.get(SESSION_MUTEX_NAME); 149 if (mutex is null) { 150 mutex = cast(Object)this.attributes; 151 } 152 return mutex; 153 } 154 155 /** 156 * Whether the {@link #sessionCompleted()} was already invoked. 157 */ 158 bool isSessionCompleted() { 159 return (this.attributes.get(SESSION_COMPLETED_NAME) !is null); 160 } 161 162 /** 163 * Invoked when the session is completed. Executed completion callbacks. 164 */ 165 void sessionCompleted() { 166 synchronized (getSessionMutex()) { 167 if (!isSessionCompleted()) { 168 executeDestructionCallbacks(); 169 this.attributes.put(SESSION_COMPLETED_NAME, Boolean.TRUE); 170 } 171 } 172 } 173 174 private void executeDestructionCallbacks() { 175 foreach(string key, Object value; this.attributes) { 176 if (key.startsWith(DESTRUCTION_CALLBACK_NAME_PREFIX)) { 177 try { 178 (cast(Runnable) value).run(); 179 } 180 catch (Throwable ex) { 181 errorf("Uncaught error in session attribute destruction callback", ex); 182 } 183 } 184 } 185 } 186 187 188 /** 189 * Extract the SiMP session attributes from the given message and 190 * wrap them in a {@link SimpAttributes} instance. 191 * @param message the message to extract session attributes from 192 */ 193 static SimpAttributes fromMessage(T)(Message!T message) { 194 assert(message, "Message must not be null"); 195 MessageHeaders headers = message.getHeaders(); 196 string sessionId = SimpMessageHeaderAccessor.getSessionId(headers); 197 Map!(string, Object) sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(headers); 198 if (sessionId is null) { 199 throw new IllegalStateException("No session id in " ~ (cast(Object)message).toString()); 200 } 201 if (sessionAttributes is null) { 202 throw new IllegalStateException("No session attributes in " ~ (cast(Object)message).toString()); 203 } 204 return new SimpAttributes(sessionId, sessionAttributes); 205 } 206 207 }