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 }