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