/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.activemq.artemis.rest.topic; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.client.ClientSession; import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; import org.apache.activemq.artemis.rest.ActiveMQRestLogger; import org.apache.activemq.artemis.rest.queue.AcknowledgedQueueConsumer; import org.apache.activemq.artemis.rest.queue.Acknowledgement; import org.apache.activemq.artemis.rest.queue.DestinationServiceManager; import org.apache.activemq.artemis.rest.queue.QueueConsumer; import org.apache.activemq.artemis.rest.util.TimeoutTask; public class SubscriptionsResource implements TimeoutTask.Callback { protected ConcurrentMap<String, QueueConsumer> queueConsumers = new ConcurrentHashMap<>(); protected ClientSessionFactory sessionFactory; protected String destination; protected final String startup = Long.toString(System.currentTimeMillis()); protected AtomicLong sessionCounter = new AtomicLong(1); protected int consumerTimeoutSeconds; protected DestinationServiceManager serviceManager; public DestinationServiceManager getServiceManager() { return serviceManager; } public void setServiceManager(DestinationServiceManager serviceManager) { this.serviceManager = serviceManager; } public int getConsumerTimeoutSeconds() { return consumerTimeoutSeconds; } public void setConsumerTimeoutSeconds(int consumerTimeoutSeconds) { this.consumerTimeoutSeconds = consumerTimeoutSeconds; } public ClientSessionFactory getSessionFactory() { return sessionFactory; } public void setSessionFactory(ClientSessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public String getDestination() { return destination; } public void setDestination(String destination) { this.destination = destination; } @Override public boolean testTimeout(String target, boolean autoShutdown) { QueueConsumer consumer = queueConsumers.get(target); Subscription subscription = (Subscription) consumer; if (consumer == null) return false; if (System.currentTimeMillis() - consumer.getLastPingTime() > subscription.getTimeout()) { ActiveMQRestLogger.LOGGER.shutdownRestSubscription(consumer.getId()); if (autoShutdown) { shutdown(consumer); } return true; } else { return false; } } @Override public void shutdown(String target) { QueueConsumer consumer = queueConsumers.get(target); if (consumer == null) return; shutdown(consumer); } private void shutdown(QueueConsumer consumer) { synchronized (consumer) { consumer.shutdown(); queueConsumers.remove(consumer.getId()); Subscription subscription = (Subscription) consumer; if (subscription.isDeleteWhenIdle()) deleteSubscriberQueue(consumer); } } public void stop() { for (QueueConsumer consumer : queueConsumers.values()) { consumer.shutdown(); Subscription subscription = (Subscription) consumer; if (!subscription.isDurable()) { deleteSubscriberQueue(consumer); } } queueConsumers.clear(); } protected String generateSubscriptionName() { return startup + "-" + sessionCounter.getAndIncrement() + "-" + destination; } @POST public Response createSubscription(@FormParam("durable") @DefaultValue("false") boolean durable, @FormParam("autoAck") @DefaultValue("true") boolean autoAck, @FormParam("name") String subscriptionName, @FormParam("selector") String selector, @FormParam("delete-when-idle") Boolean destroyWhenIdle, @FormParam("idle-timeout") Long timeout, @Context UriInfo uriInfo) { ActiveMQRestLogger.LOGGER.debug("Handling POST request for \"" + uriInfo.getPath() + "\""); if (timeout == null) timeout = Long.valueOf(consumerTimeoutSeconds * 1000); boolean deleteWhenIdle = !durable; // default is true if non-durable if (destroyWhenIdle != null) deleteWhenIdle = destroyWhenIdle.booleanValue(); if (subscriptionName != null) { // see if this is a reconnect QueueConsumer consumer = queueConsumers.get(subscriptionName); if (consumer != null) { boolean acked = consumer instanceof AcknowledgedSubscriptionResource; acked = !acked; if (acked != autoAck) { throw new WebApplicationException(Response.status(412).entity("Consumer already exists and ack-modes don't match.").type("text/plain").build()); } Subscription sub = (Subscription) consumer; if (sub.isDurable() != durable) { throw new WebApplicationException(Response.status(412).entity("Consumer already exists and durability doesn't match.").type("text/plain").build()); } Response.ResponseBuilder builder = Response.noContent(); String pathToPullSubscriptions = uriInfo.getMatchedURIs().get(0); if (autoAck) { headAutoAckSubscriptionResponse(uriInfo, consumer, builder, pathToPullSubscriptions); consumer.setSessionLink(builder, uriInfo, pathToPullSubscriptions + "/auto-ack/" + consumer.getId()); } else { headAcknowledgedConsumerResponse(uriInfo, (AcknowledgedQueueConsumer) consumer, builder); consumer.setSessionLink(builder, uriInfo, pathToPullSubscriptions + "/acknowledged/" + consumer.getId()); } return builder.build(); } } else { subscriptionName = generateSubscriptionName(); } ClientSession session = null; try { // if this is not a reconnect, create the subscription queue if (!subscriptionExists(subscriptionName)) { session = sessionFactory.createSession(); if (durable) { session.createQueue(destination, subscriptionName, true); } else { session.createTemporaryQueue(destination, subscriptionName); } } QueueConsumer consumer = createConsumer(durable, autoAck, subscriptionName, selector, timeout, deleteWhenIdle); queueConsumers.put(consumer.getId(), consumer); serviceManager.getTimeoutTask().add(this, consumer.getId()); UriBuilder location = uriInfo.getAbsolutePathBuilder(); if (autoAck) location.path("auto-ack"); else location.path("acknowledged"); location.path(consumer.getId()); Response.ResponseBuilder builder = Response.created(location.build()); if (autoAck) { QueueConsumer.setConsumeNextLink(serviceManager.getLinkStrategy(), builder, uriInfo, uriInfo.getMatchedURIs().get(0) + "/auto-ack/" + consumer.getId(), "-1"); } else { AcknowledgedQueueConsumer.setAcknowledgeNextLink(serviceManager.getLinkStrategy(), builder, uriInfo, uriInfo.getMatchedURIs().get(0) + "/acknowledged/" + consumer.getId(), "-1"); } return builder.build(); } catch (ActiveMQException e) { throw new RuntimeException(e); } finally { if (session != null) { try { session.close(); } catch (ActiveMQException e) { } } } } protected QueueConsumer createConsumer(boolean durable, boolean autoAck, String subscriptionName, String selector, long timeout, boolean deleteWhenIdle) throws ActiveMQException { QueueConsumer consumer; if (autoAck) { SubscriptionResource subscription = new SubscriptionResource(sessionFactory, subscriptionName, subscriptionName, serviceManager, selector, durable, timeout); subscription.setDurable(durable); subscription.setDeleteWhenIdle(deleteWhenIdle); consumer = subscription; } else { AcknowledgedSubscriptionResource subscription = new AcknowledgedSubscriptionResource(sessionFactory, subscriptionName, subscriptionName, serviceManager, selector, durable, timeout); subscription.setDurable(durable); subscription.setDeleteWhenIdle(deleteWhenIdle); consumer = subscription; } return consumer; } @Path("auto-ack/{consumer-id}") @GET public Response getAutoAckSubscription(@PathParam("consumer-id") String consumerId, @Context UriInfo uriInfo) throws Exception { ActiveMQRestLogger.LOGGER.debug("Handling GET request for \"" + uriInfo.getPath() + "\""); return internalHeadAutoAckSubscription(uriInfo, consumerId); } @Path("auto-ack/{consumer-id}") @HEAD public Response headAutoAckSubscription(@PathParam("consumer-id") String consumerId, @Context UriInfo uriInfo) throws Exception { ActiveMQRestLogger.LOGGER.debug("Handling HEAD request for \"" + uriInfo.getPath() + "\""); return internalHeadAutoAckSubscription(uriInfo, consumerId); } private Response internalHeadAutoAckSubscription(UriInfo uriInfo, String consumerId) { QueueConsumer consumer = findAutoAckSubscription(consumerId); Response.ResponseBuilder builder = Response.noContent(); String pathToPullSubscriptions = uriInfo.getMatchedURIs().get(1); headAutoAckSubscriptionResponse(uriInfo, consumer, builder, pathToPullSubscriptions); return builder.build(); } private void headAutoAckSubscriptionResponse(UriInfo uriInfo, QueueConsumer consumer, Response.ResponseBuilder builder, String pathToPullSubscriptions) { // we synchronize just in case a failed request is still processing synchronized (consumer) { QueueConsumer.setConsumeNextLink(serviceManager.getLinkStrategy(), builder, uriInfo, pathToPullSubscriptions + "/acknowledged/" + consumer.getId(), Long.toString(consumer.getConsumeIndex())); } } @Path("auto-ack/{subscription-id}") public QueueConsumer findAutoAckSubscription(@PathParam("subscription-id") String subscriptionId) { QueueConsumer consumer = queueConsumers.get(subscriptionId); if (consumer == null) { consumer = recreateTopicConsumer(subscriptionId, true); } return consumer; } @Path("acknowledged/{consumer-id}") @GET public Response getAcknowledgedConsumer(@PathParam("consumer-id") String consumerId, @Context UriInfo uriInfo) throws Exception { ActiveMQRestLogger.LOGGER.debug("Handling GET request for \"" + uriInfo.getPath() + "\""); return internalHeadAcknowledgedConsumer(uriInfo, consumerId); } @Path("acknowledged/{consumer-id}") @HEAD public Response headAcknowledgedConsumer(@PathParam("consumer-id") String consumerId, @Context UriInfo uriInfo) throws Exception { ActiveMQRestLogger.LOGGER.debug("Handling HEAD request for \"" + uriInfo.getPath() + "\""); return internalHeadAcknowledgedConsumer(uriInfo, consumerId); } private Response internalHeadAcknowledgedConsumer(UriInfo uriInfo, String consumerId) { AcknowledgedQueueConsumer consumer = (AcknowledgedQueueConsumer) findAcknoledgeSubscription(consumerId); Response.ResponseBuilder builder = Response.ok(); headAcknowledgedConsumerResponse(uriInfo, consumer, builder); return builder.build(); } private void headAcknowledgedConsumerResponse(UriInfo uriInfo, AcknowledgedQueueConsumer consumer, Response.ResponseBuilder builder) { // we synchronize just in case a failed request is still processing synchronized (consumer) { Acknowledgement ack = consumer.getAck(); if (ack == null || ack.wasSet()) { AcknowledgedQueueConsumer.setAcknowledgeNextLink(serviceManager.getLinkStrategy(), builder, uriInfo, uriInfo.getMatchedURIs().get(1) + "/acknowledged/" + consumer.getId(), Long.toString(consumer.getConsumeIndex())); } else { consumer.setAcknowledgementLink(builder, uriInfo, uriInfo.getMatchedURIs().get(1) + "/acknowledged/" + consumer.getId()); } } } @Path("acknowledged/{subscription-id}") public QueueConsumer findAcknoledgeSubscription(@PathParam("subscription-id") String subscriptionId) { QueueConsumer consumer = queueConsumers.get(subscriptionId); if (consumer == null) { consumer = recreateTopicConsumer(subscriptionId, false); } return consumer; } private boolean subscriptionExists(String subscriptionId) { ClientSession session = null; try { session = sessionFactory.createSession(); ClientSession.QueueQuery query = session.queueQuery(new SimpleString(subscriptionId)); return query.isExists(); } catch (ActiveMQException e) { throw new RuntimeException(e); } finally { if (session != null) { try { session.close(); } catch (ActiveMQException e) { } } } } private QueueConsumer recreateTopicConsumer(String subscriptionId, boolean autoAck) { QueueConsumer consumer; if (subscriptionExists(subscriptionId)) { QueueConsumer tmp = null; try { tmp = createConsumer(true, autoAck, subscriptionId, null, consumerTimeoutSeconds * 1000L, false); } catch (ActiveMQException e) { throw new RuntimeException(e); } consumer = queueConsumers.putIfAbsent(subscriptionId, tmp); if (consumer == null) { consumer = tmp; serviceManager.getTimeoutTask().add(this, subscriptionId); } else { tmp.shutdown(); } } else { throw new WebApplicationException(Response.status(405).entity("Failed to find subscriber " + subscriptionId + " you will have to reconnect").type("text/plain").build()); } return consumer; } @Path("acknowledged/{subscription-id}") @DELETE public void deleteAckSubscription(@Context UriInfo uriInfo, @PathParam("subscription-id") String consumerId) { ActiveMQRestLogger.LOGGER.debug("Handling DELETE request for \"" + uriInfo.getPath() + "\""); internalDeleteSubscription(consumerId); } @Path("auto-ack/{subscription-id}") @DELETE public void deleteSubscription(@Context UriInfo uriInfo, @PathParam("subscription-id") String consumerId) { ActiveMQRestLogger.LOGGER.debug("Handling DELETE request for \"" + uriInfo.getPath() + "\""); internalDeleteSubscription(consumerId); } private void internalDeleteSubscription(String consumerId) { QueueConsumer consumer = queueConsumers.remove(consumerId); if (consumer == null) { String msg = "Failed to match a subscription to URL " + consumerId; throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).entity(msg).type("text/plain").build()); } consumer.shutdown(); deleteSubscriberQueue(consumer); } private void deleteSubscriberQueue(QueueConsumer consumer) { String subscriptionName = consumer.getId(); ClientSession session = null; try { session = sessionFactory.createSession(); session.deleteQueue(subscriptionName); } catch (ActiveMQException e) { } finally { if (session != null) { try { session.close(); } catch (ActiveMQException e) { } } } } }