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}