/*
* 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.core.protocol.stomp;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.zip.Inflater;
import io.netty.buffer.UnpooledByteBufAllocator;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ICoreMessage;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.Pair;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.core.buffers.impl.ChannelBufferWrapper;
import org.apache.activemq.artemis.core.message.LargeBodyEncoder;
import org.apache.activemq.artemis.core.message.impl.CoreMessage;
import org.apache.activemq.artemis.core.persistence.OperationContext;
import org.apache.activemq.artemis.core.persistence.StorageManager;
import org.apache.activemq.artemis.core.persistence.impl.journal.LargeServerMessageImpl;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.server.LargeServerMessage;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.core.server.ServerConsumer;
import org.apache.activemq.artemis.core.server.ServerSession;
import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.protocol.SessionCallback;
import org.apache.activemq.artemis.spi.core.remoting.ReadyListener;
import org.apache.activemq.artemis.utils.ByteUtil;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.PendingTask;
import org.apache.activemq.artemis.utils.UUIDGenerator;
import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProtocolMessageBundle.BUNDLE;
public class StompSession implements SessionCallback {
private final StompProtocolManager manager;
private final StompConnection connection;
private ServerSession session;
private final OperationContext sessionContext;
private final BlockingDeque<PendingTask> afterDeliveryTasks = new LinkedBlockingDeque<>();
private final Map<Long, StompSubscription> subscriptions = new ConcurrentHashMap<>();
// key = message ID, value = consumer ID
private final Map<Long, Pair<Long, Integer>> messagesToAck = new ConcurrentHashMap<>();
private volatile boolean noLocal = false;
private final int consumerCredits;
StompSession(final StompConnection connection, final StompProtocolManager manager, OperationContext sessionContext) {
this.connection = connection;
this.manager = manager;
this.sessionContext = sessionContext;
this.consumerCredits = ConfigurationHelper.getIntProperty(TransportConstants.STOMP_CONSUMERS_CREDIT, TransportConstants.STOMP_DEFAULT_CONSUMERS_CREDIT, connection.getAcceptorUsed().getConfiguration());
}
@Override
public boolean isWritable(ReadyListener callback, Object protocolContext) {
return connection.isWritable(callback);
}
void setServerSession(ServerSession session) {
this.session = session;
}
public ServerSession getCoreSession() {
return session;
}
@Override
public boolean hasCredits(ServerConsumer consumerID) {
return true;
}
@Override
public void sendProducerCreditsMessage(int credits, SimpleString address) {
}
@Override
public void sendProducerCreditsFailMessage(int credits, SimpleString address) {
}
@Override
public void afterDelivery() throws Exception {
PendingTask task;
while ((task = afterDeliveryTasks.poll()) != null) {
task.run();
}
}
@Override
public void browserFinished(ServerConsumer consumer) {
}
@Override
public boolean updateDeliveryCountAfterCancel(ServerConsumer consumer, MessageReference ref, boolean failed) {
return false;
}
@Override
public int sendMessage(MessageReference ref,
Message serverMessage,
final ServerConsumer consumer,
int deliveryCount) {
ICoreMessage coreMessage = serverMessage.toCore();
LargeServerMessageImpl largeMessage = null;
ICoreMessage newServerMessage = serverMessage.toCore();
try {
StompSubscription subscription = subscriptions.get(consumer.getID());
StompFrame frame;
ActiveMQBuffer buffer;
if (coreMessage.isLargeMessage()) {
LargeBodyEncoder encoder = coreMessage.getBodyEncoder();
encoder.open();
int bodySize = (int) encoder.getLargeBodySize();
buffer = new ChannelBufferWrapper(UnpooledByteBufAllocator.DEFAULT.heapBuffer(bodySize));
encoder.encode(buffer, bodySize);
encoder.close();
} else {
buffer = coreMessage.getReadOnlyBodyBuffer();
}
if (serverMessage.getBooleanProperty(Message.HDR_LARGE_COMPRESSED)) {
ActiveMQBuffer qbuff = buffer;
int bytesToRead = qbuff.readerIndex();
Inflater inflater = new Inflater();
inflater.setInput(ByteUtil.getActiveArray(qbuff.readBytes(bytesToRead).toByteBuffer()));
//get the real size of large message
long sizeBody = newServerMessage.getLongProperty(Message.HDR_LARGE_BODY_SIZE);
byte[] data = new byte[(int) sizeBody];
inflater.inflate(data);
inflater.end();
qbuff.resetReaderIndex();
qbuff.resetWriterIndex();
qbuff.writeBytes(data);
buffer = qbuff;
}
frame = connection.createStompMessage(newServerMessage, buffer, subscription, deliveryCount);
int length = frame.getEncodedSize();
if (subscription.getAck().equals(Stomp.Headers.Subscribe.AckModeValues.AUTO)) {
if (manager.send(connection, frame)) {
final long messageID = newServerMessage.getMessageID();
final long consumerID = consumer.getID();
// this will be called after the delivery is complete
// we can't call session.ack within the delivery
// as it could dead lock.
afterDeliveryTasks.offer(new PendingTask() {
@Override
public void run() throws Exception {
//we ack and commit only if the send is successful
session.acknowledge(consumerID, messageID);
session.commit();
}
});
}
} else {
messagesToAck.put(newServerMessage.getMessageID(), new Pair<>(consumer.getID(), length));
// Must send AFTER adding to messagesToAck - or could get acked from client BEFORE it's been added!
manager.send(connection, frame);
}
return length;
} catch (Exception e) {
if (ActiveMQStompProtocolLogger.LOGGER.isDebugEnabled()) {
ActiveMQStompProtocolLogger.LOGGER.debug(e);
}
return 0;
} finally {
if (largeMessage != null) {
largeMessage.releaseResources();
largeMessage = null;
}
}
}
@Override
public int sendLargeMessageContinuation(ServerConsumer consumer,
byte[] body,
boolean continues,
boolean requiresResponse) {
return 0;
}
@Override
public int sendLargeMessage(MessageReference ref,
Message msg,
ServerConsumer consumer,
long bodySize,
int deliveryCount) {
return 0;
}
@Override
public void closed() {
}
@Override
public void disconnect(ServerConsumer consumerId, String queueName) {
StompSubscription stompSubscription = subscriptions.remove(consumerId.getID());
if (stompSubscription != null) {
StompFrame frame = connection.getFrameHandler().createStompFrame(Stomp.Responses.ERROR);
frame.addHeader(Stomp.Headers.CONTENT_TYPE, "text/plain");
frame.setBody("consumer with ID " + consumerId + " disconnected by server");
connection.sendFrame(frame);
}
}
public void acknowledge(String messageID, String subscriptionID) throws Exception {
long id = Long.parseLong(messageID);
Pair<Long, Integer> pair = messagesToAck.remove(id);
if (pair == null) {
throw BUNDLE.failToAckMissingID(id).setHandler(connection.getFrameHandler());
}
long consumerID = pair.getA();
int credits = pair.getB();
StompSubscription sub = subscriptions.get(consumerID);
if (subscriptionID != null) {
if (!sub.getID().equals(subscriptionID)) {
throw BUNDLE.subscriptionIDMismatch(subscriptionID, sub.getID()).setHandler(connection.getFrameHandler());
}
}
if (this.consumerCredits != -1) {
session.receiveConsumerCredits(consumerID, credits);
}
if (sub.getAck().equals(Stomp.Headers.Subscribe.AckModeValues.CLIENT_INDIVIDUAL)) {
session.individualAcknowledge(consumerID, id);
} else {
session.acknowledge(consumerID, id);
}
session.commit();
}
public void addSubscription(long consumerID,
String subscriptionID,
String clientID,
String durableSubscriptionName,
String destination,
String selector,
String ack) throws Exception {
SimpleString queueName = SimpleString.toSimpleString(destination);
boolean pubSub = false;
int receiveCredits = consumerCredits;
if (ack.equals(Stomp.Headers.Subscribe.AckModeValues.AUTO)) {
receiveCredits = -1;
}
Set<RoutingType> routingTypes = manager.getServer().getAddressInfo(getCoreSession().removePrefix(SimpleString.toSimpleString(destination))).getRoutingTypes();
if (routingTypes.size() == 1 && routingTypes.contains(RoutingType.MULTICAST)) {
// subscribes to a topic
pubSub = true;
if (durableSubscriptionName != null) {
if (clientID == null) {
throw BUNDLE.missingClientID();
}
queueName = SimpleString.toSimpleString(clientID + "." + durableSubscriptionName);
if (manager.getServer().locateQueue(queueName) == null) {
session.createQueue(SimpleString.toSimpleString(destination), queueName, SimpleString.toSimpleString(selector), false, true);
}
} else {
queueName = UUIDGenerator.getInstance().generateSimpleStringUUID();
session.createQueue(SimpleString.toSimpleString(destination), queueName, SimpleString.toSimpleString(selector), true, false);
}
session.createConsumer(consumerID, queueName, null, false, false, receiveCredits);
} else {
session.createConsumer(consumerID, queueName, SimpleString.toSimpleString(selector), false, false, receiveCredits);
}
StompSubscription subscription = new StompSubscription(subscriptionID, ack, queueName, pubSub);
subscriptions.put(consumerID, subscription);
session.start();
}
public boolean unsubscribe(String id, String durableSubscriptionName, String clientID) throws Exception {
boolean result = false;
Iterator<Entry<Long, StompSubscription>> iterator = subscriptions.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, StompSubscription> entry = iterator.next();
long consumerID = entry.getKey();
StompSubscription sub = entry.getValue();
if (id != null && id.equals(sub.getID())) {
iterator.remove();
SimpleString queueName = sub.getQueueName();
session.closeConsumer(consumerID);
if (sub.isPubSub() && manager.getServer().locateQueue(queueName) != null) {
session.deleteQueue(queueName);
}
result = true;
}
}
if (!result && durableSubscriptionName != null && clientID != null) {
SimpleString queueName = SimpleString.toSimpleString(clientID + "." + durableSubscriptionName);
if (manager.getServer().locateQueue(queueName) != null) {
session.deleteQueue(queueName);
}
result = true;
}
return result;
}
boolean containsSubscription(String subscriptionID) {
for (Entry<Long, StompSubscription> entry : subscriptions.entrySet()) {
StompSubscription sub = entry.getValue();
if (sub.getID().equals(subscriptionID)) {
return true;
}
}
return false;
}
public RemotingConnection getConnection() {
return connection;
}
public OperationContext getContext() {
return sessionContext;
}
public boolean isNoLocal() {
return noLocal;
}
public void setNoLocal(boolean noLocal) {
this.noLocal = noLocal;
}
public void sendInternal(Message message, boolean direct) throws Exception {
session.send(message, direct);
}
public void sendInternalLarge(CoreMessage message, boolean direct) throws Exception {
int headerSize = message.getHeadersAndPropertiesEncodeSize();
if (headerSize >= connection.getMinLargeMessageSize()) {
throw BUNDLE.headerTooBig();
}
StorageManager storageManager = ((ServerSessionImpl) session).getStorageManager();
long id = storageManager.generateID();
LargeServerMessage largeMessage = storageManager.createLargeMessage(id, message);
byte[] bytes = new byte[message.getBodyBuffer().writerIndex() - CoreMessage.BODY_OFFSET];
message.getBodyBuffer().readBytes(bytes);
largeMessage.addBytes(bytes);
largeMessage.releaseResources();
largeMessage.putLongProperty(Message.HDR_LARGE_BODY_SIZE, bytes.length);
session.send(largeMessage, direct);
largeMessage = null;
}
}