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}