View Javadoc
1   /*
2    * Copyright 2002-2014 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  package org.springframework.messaging.simp.stomp;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.DataOutputStream;
21  import java.io.IOException;
22  import java.util.Arrays;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Map.Entry;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  
30  import org.springframework.messaging.Message;
31  import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
32  import org.springframework.messaging.simp.SimpMessageType;
33  import org.springframework.messaging.support.NativeMessageHeaderAccessor;
34  import org.springframework.util.Assert;
35  
36  /**
37   * An encoder for STOMP frames.
38   *
39   * @author Andy Wilkinson
40   * @author Rossen Stoyanchev
41   * @since 4.0
42   */
43  public final class StompEncoder  {
44  
45  	private static final byte LF = '\n';
46  
47  	private static final byte COLON = ':';
48  
49  	private final Log logger = LogFactory.getLog(StompEncoder.class);
50  
51  
52  	/**
53  	 * Encodes the given STOMP {@code message} into a {@code byte[]}
54  	 * @param message the message to encode
55  	 * @return the encoded message
56  	 */
57  	public byte[] encode(Message<byte[]> message) {
58  		return encode(message.getHeaders(), message.getPayload());
59  	}
60  
61  	/**
62  	 * Encodes the given payload and headers into a {@code byte[]}.
63  	 * @param headers the headers
64  	 * @param payload the payload
65  	 * @return the encoded message
66  	 */
67  	public byte[] encode(Map<String, Object> headers, byte[] payload) {
68  		Assert.notNull(headers, "'headers' is required");
69  		Assert.notNull(payload, "'payload' is required");
70  
71  		try {
72  			ByteArrayOutputStream baos = new ByteArrayOutputStream(128 + payload.length);
73  			DataOutputStream output = new DataOutputStream(baos);
74  
75  			if (SimpMessageType.HEARTBEAT.equals(SimpMessageHeaderAccessor.getMessageType(headers))) {
76  				output.write(StompDecoder.HEARTBEAT_PAYLOAD);
77  			}
78  			else {
79  				StompCommand command = StompHeaderAccessor.getCommand(headers);
80  				Assert.notNull(command, "Missing STOMP command: " + headers);
81  				output.write(command.toString().getBytes(StompDecoder.UTF8_CHARSET));
82  				output.write(LF);
83  				writeHeaders(command, headers, payload, output);
84  				output.write(LF);
85  				writeBody(payload, output);
86  				output.write((byte) 0);
87  			}
88  
89  			return baos.toByteArray();
90  		}
91  		catch (IOException ex) {
92  			throw new StompConversionException("Failed to encode STOMP frame, headers=" + headers,  ex);
93  		}
94  	}
95  
96  	private void writeHeaders(StompCommand command, Map<String, Object> headers, byte[] payload, DataOutputStream output)
97  			throws IOException {
98  
99  		@SuppressWarnings("unchecked")
100 		Map<String,List<String>> nativeHeaders =
101 				(Map<String, List<String>>) headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS);
102 
103 		if (logger.isTraceEnabled()) {
104 			logger.trace("Encoding STOMP " + command + ", headers=" + nativeHeaders);
105 		}
106 
107 		if (nativeHeaders == null) {
108 			return;
109 		}
110 
111 		boolean shouldEscape = (command != StompCommand.CONNECT && command != StompCommand.CONNECTED);
112 
113 		for (Entry<String, List<String>> entry : nativeHeaders.entrySet()) {
114 			byte[] key = encodeHeaderString(entry.getKey(), shouldEscape);
115 			if (command.requiresContentLength() && "content-length".equals(entry.getKey())) {
116 				continue;
117 			}
118 			List<String> values = entry.getValue();
119 			if (StompCommand.CONNECT.equals(command) &&
120 					StompHeaderAccessor.STOMP_PASSCODE_HEADER.equals(entry.getKey())) {
121 				values = Arrays.asList(StompHeaderAccessor.getPasscode(headers));
122 			}
123 			for (String value : values) {
124 				output.write(key);
125 				output.write(COLON);
126 				output.write(encodeHeaderString(value, shouldEscape));
127 				output.write(LF);
128 			}
129 		}
130 		if (command.requiresContentLength()) {
131 			int contentLength = payload.length;
132 			output.write("content-length:".getBytes(StompDecoder.UTF8_CHARSET));
133 			output.write(Integer.toString(contentLength).getBytes(StompDecoder.UTF8_CHARSET));
134 			output.write(LF);
135 		}
136 	}
137 
138 	private byte[] encodeHeaderString(String input, boolean escape) {
139 		String inputToUse = (escape ? escape(input) : input);
140 		return inputToUse.getBytes(StompDecoder.UTF8_CHARSET);
141 	}
142 
143 	/**
144 	 * See STOMP Spec 1.2:
145 	 * <a href="http://stomp.github.io/stomp-specification-1.2.html#Value_Encoding">"Value Encoding"</a>.
146 	 */
147 	private String escape(String inString) {
148 		StringBuilder sb = new StringBuilder(inString.length());
149 		for (int i = 0; i < inString.length(); i++) {
150 			char c = inString.charAt(i);
151 			if (c == '\\') {
152 				sb.append("\\\\");
153 			}
154 			else if (c == ':') {
155 				sb.append("\\c");
156 			}
157 			else if (c == '\n') {
158 				 sb.append("\\n");
159 			}
160 			else if (c == '\r') {
161 				sb.append("\\r");
162 			}
163 			else {
164 				sb.append(c);
165 			}
166 		}
167 		return sb.toString();
168 	}
169 
170 	private void writeBody(byte[] payload, DataOutputStream output) throws IOException {
171 		output.write(payload);
172 	}
173 
174 }