001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * <p/> 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * <p/> 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.activemq.web; 019 020import org.apache.activemq.MessageAvailableConsumer; 021import org.apache.activemq.MessageAvailableListener; 022import org.eclipse.jetty.continuation.Continuation; 023import org.eclipse.jetty.continuation.ContinuationSupport; 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027import javax.jms.*; 028import javax.servlet.ServletConfig; 029import javax.servlet.ServletException; 030import javax.servlet.http.HttpServletRequest; 031import javax.servlet.http.HttpServletResponse; 032import java.io.IOException; 033import java.io.PrintWriter; 034import java.util.Enumeration; 035import java.util.Map; 036import java.util.Set; 037import java.util.concurrent.ConcurrentHashMap; 038 039/** 040 * A servlet for sending and receiving messages to/from JMS destinations using 041 * HTTP POST for sending and HTTP GET for receiving. 042 * <p/> 043 * You can specify the destination and whether it is a topic or queue via 044 * configuration details on the servlet or as request parameters. 045 * <p/> 046 * For reading messages you can specify a readTimeout parameter to determine how 047 * long the servlet should block for. 048 * 049 * One thing to keep in mind with this solution - due to the nature of REST, 050 * there will always be a chance of losing messages. Consider what happens when 051 * a message is retrieved from the broker but the web call is interrupted before 052 * the client receives the message in the response - the message is lost. 053 */ 054public class MessageServlet extends MessageServletSupport { 055 056 // its a bit pita that this servlet got intermixed with jetty continuation/rest 057 // instead of creating a special for that. We should have kept a simple servlet 058 // for good old fashioned request/response blocked communication. 059 060 private static final long serialVersionUID = 8737914695188481219L; 061 062 private static final Logger LOG = LoggerFactory.getLogger(MessageServlet.class); 063 064 private final String readTimeoutParameter = "readTimeout"; 065 private final String readTimeoutRequestAtt = "xamqReadDeadline"; 066 private final String oneShotParameter = "oneShot"; 067 private long defaultReadTimeout = -1; 068 private long maximumReadTimeout = 20000; 069 private long requestTimeout = 1000; 070 private String defaultContentType; 071 072 private final Map<String, WebClient> clients = new ConcurrentHashMap<>(); 073 private final Set<MessageAvailableConsumer> activeConsumers = ConcurrentHashMap.newKeySet(); 074 075 @Override 076 public void init() throws ServletException { 077 ServletConfig servletConfig = getServletConfig(); 078 String name = servletConfig.getInitParameter("defaultReadTimeout"); 079 if (name != null) { 080 defaultReadTimeout = asLong(name); 081 } 082 name = servletConfig.getInitParameter("maximumReadTimeout"); 083 if (name != null) { 084 maximumReadTimeout = asLong(name); 085 } 086 name = servletConfig.getInitParameter("replyTimeout"); 087 if (name != null) { 088 requestTimeout = asLong(name); 089 } 090 name = servletConfig.getInitParameter("defaultContentType"); 091 if (name != null) { 092 defaultContentType = name; 093 } 094 } 095 096 /** 097 * Sends a message to a destination 098 * 099 * @param request 100 * @param response 101 * @throws ServletException 102 * @throws IOException 103 */ 104 @Override 105 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 106 // lets turn the HTTP post into a JMS Message 107 try { 108 109 String action = request.getParameter("action"); 110 String clientId = request.getParameter("clientId"); 111 if (clientId != null && "unsubscribe".equals(action)) { 112 LOG.info("Unsubscribing client " + clientId); 113 WebClient client = getWebClient(request); 114 client.close(); 115 clients.remove(clientId); 116 return; 117 } 118 119 WebClient client = getWebClient(request); 120 121 String text = getPostedMessageBody(request); 122 123 // lets create the destination from the URI? 124 Destination destination = getDestination(client, request); 125 if (destination == null) { 126 throw new NoDestinationSuppliedException(); 127 } 128 129 if (LOG.isDebugEnabled()) { 130 LOG.debug("Sending message to: " + destination + " with text: " + text); 131 } 132 133 boolean sync = isSync(request); 134 TextMessage message = client.getSession().createTextMessage(text); 135 136 appendParametersToMessage(request, message); 137 boolean persistent = isSendPersistent(request); 138 int priority = getSendPriority(request); 139 long timeToLive = getSendTimeToLive(request); 140 client.send(destination, message, persistent, priority, timeToLive); 141 142 // lets return a unique URI for reliable messaging 143 response.setHeader("messageID", message.getJMSMessageID()); 144 response.setStatus(HttpServletResponse.SC_OK); 145 response.getWriter().write("Message sent"); 146 147 } catch (JMSException e) { 148 throw new ServletException("Could not post JMS message: " + e, e); 149 } 150 } 151 152 /** 153 * Supports a HTTP DELETE to be equivalent of consuming a single message 154 * from a queue 155 */ 156 @Override 157 protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 158 doMessages(request, response); 159 } 160 161 /** 162 * Supports a HTTP DELETE to be equivalent of consuming a single message 163 * from a queue 164 */ 165 @Override 166 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 167 doMessages(request, response); 168 } 169 170 /** 171 * Reads a message from a destination up to some specific timeout period 172 * 173 * @param request 174 * @param response 175 * @throws ServletException 176 * @throws IOException 177 */ 178 protected void doMessages(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 179 MessageAvailableConsumer consumer = null; 180 181 try { 182 WebClient client = getWebClient(request); 183 Destination destination = getDestination(client, request); 184 if (destination == null) { 185 throw new NoDestinationSuppliedException(); 186 } 187 consumer = (MessageAvailableConsumer) client.getConsumer(destination, request.getHeader(WebClient.selectorName)); 188 Continuation continuation = ContinuationSupport.getContinuation(request); 189 190 // Don't allow concurrent use of the consumer. Do make sure to allow 191 // subsequent calls on continuation to use the consumer. 192 if (continuation.isInitial() && !activeConsumers.add(consumer)) { 193 throw new ServletException("Concurrent access to consumer is not supported"); 194 } 195 196 Message message = null; 197 198 long deadline = getReadDeadline(request); 199 long timeout = deadline - System.currentTimeMillis(); 200 201 // Set the message available listener *before* calling receive to eliminate any 202 // chance of a missed notification between the time receive() completes without 203 // a message and the time the listener is set. 204 synchronized (consumer) { 205 Listener listener = (Listener) consumer.getAvailableListener(); 206 if (listener == null) { 207 listener = new Listener(consumer); 208 consumer.setAvailableListener(listener); 209 } 210 } 211 212 if (LOG.isDebugEnabled()) { 213 LOG.debug("Receiving message(s) from: " + destination + " with timeout: " + timeout); 214 } 215 216 // Look for any available messages (need a little timeout). Always 217 // try at least one lookup; don't block past the deadline. 218 if (timeout <= 0) { 219 message = consumer.receiveNoWait(); 220 } else if (timeout < 10) { 221 message = consumer.receive(timeout); 222 } else { 223 message = consumer.receive(10); 224 } 225 226 if (message == null) { 227 handleContinuation(request, response, client, destination, consumer, deadline); 228 } else { 229 writeResponse(request, response, message); 230 closeConsumerOnOneShot(request, client, destination); 231 232 activeConsumers.remove(consumer); 233 } 234 } catch (JMSException e) { 235 throw new ServletException("Could not post JMS message: " + e, e); 236 } 237 } 238 239 protected void handleContinuation(HttpServletRequest request, HttpServletResponse response, WebClient client, Destination destination, 240 MessageAvailableConsumer consumer, long deadline) { 241 // Get an existing Continuation or create a new one if there are no events. 242 Continuation continuation = ContinuationSupport.getContinuation(request); 243 244 long timeout = deadline - System.currentTimeMillis(); 245 if ((continuation.isExpired()) || (timeout <= 0)) { 246 // Reset the continuation on the available listener for the consumer to prevent the 247 // next message receipt from being consumed without a valid, active continuation. 248 synchronized (consumer) { 249 Object obj = consumer.getAvailableListener(); 250 if (obj instanceof Listener) { 251 ((Listener) obj).setContinuation(null); 252 } 253 } 254 response.setStatus(HttpServletResponse.SC_NO_CONTENT); 255 closeConsumerOnOneShot(request, client, destination); 256 activeConsumers.remove(consumer); 257 return; 258 } 259 260 continuation.setTimeout(timeout); 261 continuation.suspend(); 262 263 synchronized (consumer) { 264 Listener listener = (Listener) consumer.getAvailableListener(); 265 266 // register this continuation with our listener. 267 listener.setContinuation(continuation); 268 } 269 } 270 271 protected void writeResponse(HttpServletRequest request, HttpServletResponse response, Message message) throws IOException, JMSException { 272 int messages = 0; 273 try { 274 response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 275 // 1.1 276 response.setHeader("Pragma", "no-cache"); // HTTP 1.0 277 response.setDateHeader("Expires", 0); 278 279 // Set content type as in request. This should be done before calling getWriter by specification 280 String type = getContentType(request); 281 282 if (type != null) { 283 response.setContentType(type); 284 } else { 285 if (defaultContentType != null) { 286 response.setContentType(defaultContentType); 287 } else if (isXmlContent(message)) { 288 response.setContentType("application/xml"); 289 } else { 290 response.setContentType("text/plain"); 291 } 292 } 293 294 // write a responds 295 PrintWriter writer = response.getWriter(); 296 297 // handle any message(s) 298 if (message == null) { 299 // No messages so OK response of for ajax else no content. 300 response.setStatus(HttpServletResponse.SC_NO_CONTENT); 301 } else { 302 // We have at least one message so set up the response 303 messages = 1; 304 305 response.setStatus(HttpServletResponse.SC_OK); 306 307 setResponseHeaders(response, message); 308 writeMessageResponse(writer, message); 309 writer.flush(); 310 } 311 } finally { 312 if (LOG.isDebugEnabled()) { 313 LOG.debug("Received " + messages + " message(s)"); 314 } 315 } 316 } 317 318 protected void writeMessageResponse(PrintWriter writer, Message message) throws JMSException, IOException { 319 if (message instanceof TextMessage) { 320 TextMessage textMsg = (TextMessage) message; 321 String txt = textMsg.getText(); 322 if (txt != null) { 323 if (txt.startsWith("<?")) { 324 txt = txt.substring(txt.indexOf("?>") + 2); 325 } 326 writer.print(txt); 327 } 328 } else if (message instanceof ObjectMessage) { 329 ObjectMessage objectMsg = (ObjectMessage) message; 330 Object object = objectMsg.getObject(); 331 if (object != null) { 332 writer.print(object.toString()); 333 } 334 } 335 } 336 337 protected boolean isXmlContent(Message message) throws JMSException { 338 if (message instanceof TextMessage) { 339 TextMessage textMsg = (TextMessage) message; 340 String txt = textMsg.getText(); 341 if (txt != null) { 342 // assume its xml when it starts with < 343 if (txt.startsWith("<")) { 344 return true; 345 } 346 } 347 } 348 // for any other kind of messages we dont assume xml 349 return false; 350 } 351 352 public WebClient getWebClient(HttpServletRequest request) { 353 String clientId = request.getParameter("clientId"); 354 if (clientId != null) { 355 LOG.debug("Getting local client [" + clientId + "]"); 356 return clients.computeIfAbsent(clientId, k -> new WebClient()); 357 } else { 358 return WebClient.getWebClient(request); 359 } 360 } 361 362 protected String getContentType(HttpServletRequest request) { 363 String value = request.getParameter("xml"); 364 if (value != null && "true".equalsIgnoreCase(value)) { 365 return "application/xml"; 366 } 367 value = request.getParameter("json"); 368 if (value != null && "true".equalsIgnoreCase(value)) { 369 return "application/json"; 370 } 371 return null; 372 } 373 374 @SuppressWarnings("rawtypes") 375 protected void setResponseHeaders(HttpServletResponse response, Message message) throws JMSException { 376 response.setHeader("destination", message.getJMSDestination().toString()); 377 response.setHeader("id", message.getJMSMessageID()); 378 379 // Return JMS properties as header values. 380 for (Enumeration names = message.getPropertyNames(); names.hasMoreElements(); ) { 381 String name = (String) names.nextElement(); 382 response.setHeader(name, message.getObjectProperty(name).toString()); 383 } 384 } 385 386 /** 387 * @return the timeout value for read requests which is always >= 0 and <= 388 * maximumReadTimeout to avoid DoS attacks 389 */ 390 protected long getReadDeadline(HttpServletRequest request) { 391 Long answer; 392 393 answer = (Long) request.getAttribute(readTimeoutRequestAtt); 394 395 if (answer == null) { 396 long timeout = defaultReadTimeout; 397 String name = request.getParameter(readTimeoutParameter); 398 if (name != null) { 399 timeout = asLong(name); 400 } 401 if (timeout < 0 || timeout > maximumReadTimeout) { 402 timeout = maximumReadTimeout; 403 } 404 405 answer = Long.valueOf(System.currentTimeMillis() + timeout); 406 } 407 return answer.longValue(); 408 } 409 410 /** 411 * Close the consumer if one-shot mode is used on the given request. 412 */ 413 protected void closeConsumerOnOneShot(HttpServletRequest request, WebClient client, Destination dest) { 414 if (asBoolean(request.getParameter(oneShotParameter), false)) { 415 try { 416 client.closeConsumer(dest); 417 } catch (JMSException jms_exc) { 418 LOG.warn("JMS exception on closing consumer after request with one-shot mode", jms_exc); 419 } 420 } 421 } 422 423 /* 424 * Listen for available messages and wakeup any continuations. 425 */ 426 private static class Listener implements MessageAvailableListener { 427 MessageConsumer consumer; 428 Continuation continuation; 429 430 Listener(MessageConsumer consumer) { 431 this.consumer = consumer; 432 } 433 434 public void setContinuation(Continuation continuation) { 435 synchronized (consumer) { 436 this.continuation = continuation; 437 } 438 } 439 440 @Override 441 public void onMessageAvailable(MessageConsumer consumer) { 442 assert this.consumer == consumer; 443 444 ((MessageAvailableConsumer) consumer).setAvailableListener(null); 445 446 synchronized (this.consumer) { 447 if (continuation != null) { 448 continuation.resume(); 449 } 450 } 451 } 452 } 453}