/**
* 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 io.hawtjms.provider.amqp;
import io.hawtjms.jms.JmsDestination;
import io.hawtjms.jms.message.JmsMessage;
import io.hawtjms.jms.message.JmsOutboundMessageDispatch;
import io.hawtjms.jms.meta.JmsProducerInfo;
import io.hawtjms.provider.AsyncResult;
import io.hawtjms.util.IOExceptionSupport;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.messaging.Accepted;
import org.apache.qpid.proton.amqp.messaging.Rejected;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.messaging.Target;
import org.apache.qpid.proton.amqp.transaction.TransactionalState;
import org.apache.qpid.proton.amqp.transport.DeliveryState;
import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Sender;
import org.apache.qpid.proton.jms.AutoOutboundTransformer;
import org.apache.qpid.proton.jms.EncodedMessage;
import org.apache.qpid.proton.jms.OutboundTransformer;
import org.fusesource.hawtbuf.Buffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AMQP Producer object that is used to manage JMS MessageProducer semantics.
*
* This Producer is fixed to a given JmsDestination and can only produce messages to it.
*/
public class AmqpFixedProducer extends AmqpProducer {
private static final Logger LOG = LoggerFactory.getLogger(AmqpFixedProducer.class);
private static final byte[] EMPTY_BYTE_ARRAY = new byte[] {};
private final AmqpTransferTagGenerator tagGenerator = new AmqpTransferTagGenerator(true);
private final Set<Delivery> pending = new LinkedHashSet<Delivery>();
private final LinkedList<PendingSend> pendingSends = new LinkedList<PendingSend>();
private final OutboundTransformer outboundTransformer = new AutoOutboundTransformer(AmqpJMSVendor.INSTANCE);
private final String MESSAGE_FORMAT_KEY = outboundTransformer.getPrefixVendor() + "MESSAGE_FORMAT";
private boolean presettle = false;
public AmqpFixedProducer(AmqpSession session, JmsProducerInfo info) {
super(session, info);
}
@Override
public boolean send(JmsOutboundMessageDispatch envelope, AsyncResult<Void> request) throws IOException {
// TODO - Handle the case where remote has no credit which means we can't send to it.
// We need to hold the send until remote credit becomes available but we should
// also have a send timeout option and filter timed out sends.
if (endpoint.getCredit() <= 0) {
LOG.trace("Holding Message send until credit is available.");
// Once a message goes into a held mode we no longer can send it async, so
// we clear the async flag if set to avoid the sender never getting notified.
envelope.setSendAsync(false);
this.pendingSends.addLast(new PendingSend(envelope, request));
return false;
} else {
doSend(envelope, request);
return true;
}
}
private void doSend(JmsOutboundMessageDispatch envelope, AsyncResult<Void> request) throws IOException {
LOG.trace("Producer sending message: {}", envelope.getMessage().getFacade().getMessageId());
byte[] tag = tagGenerator.getNextTag();
Delivery delivery = null;
if (presettle) {
delivery = endpoint.delivery(EMPTY_BYTE_ARRAY, 0, 0);
} else {
delivery = endpoint.delivery(tag, 0, tag.length);
}
delivery.setContext(request);
if (session.isTransacted()) {
Binary amqpTxId = session.getTransactionContext().getAmqpTransactionId();
TransactionalState state = new TransactionalState();
state.setTxnId(amqpTxId);
delivery.disposition(state);
}
JmsMessage message = envelope.getMessage();
message.setReadOnlyBody(true);
if (!message.getProperties().containsKey(MESSAGE_FORMAT_KEY)) {
message.setProperty(MESSAGE_FORMAT_KEY, 0);
}
Buffer sendBuffer = null;
EncodedMessage amqp = null;
try {
amqp = outboundTransformer.transform(message);
} catch (Exception e) {
throw IOExceptionSupport.create(e);
}
if (amqp != null && amqp.getLength() > 0) {
sendBuffer = new Buffer(amqp.getArray(), amqp.getArrayOffset(), amqp.getLength());
}
while (sendBuffer != null) {
int sent = endpoint.send(sendBuffer.data, sendBuffer.offset, sendBuffer.length);
if (sent > 0) {
sendBuffer.moveHead(sent);
if (sendBuffer.length == 0) {
if (presettle) {
delivery.settle();
} else {
pending.add(delivery);
endpoint.advance();
}
sendBuffer = null;
if (envelope.isSendAsync() || presettle) {
request.onSuccess();
}
}
} else {
LOG.warn("{} failed to send any data from current Message.", this);
}
}
}
@Override
public void processFlowUpdates() throws IOException {
if (!pendingSends.isEmpty() && endpoint.getCredit() > 0) {
while (endpoint.getCredit() > 0 && !pendingSends.isEmpty()) {
LOG.trace("Dispatching previously held send");
PendingSend held = pendingSends.pop();
doSend(held.envelope, held.request);
}
}
}
@Override
public void processDeliveryUpdates() {
List<Delivery> toRemove = new ArrayList<Delivery>();
for (Delivery delivery : pending) {
DeliveryState state = delivery.getRemoteState();
if (state == null) {
continue;
}
@SuppressWarnings("unchecked")
AsyncResult<Void> request = (AsyncResult<Void>) delivery.getContext();
if (state instanceof Accepted) {
toRemove.add(delivery);
LOG.trace("State of delivery accepted: {}", delivery);
tagGenerator.returnTag(delivery.getTag());
if (request != null && !request.isComplete()) {
request.onSuccess();
}
} else if (state instanceof Rejected) {
Exception remoteError = getRemoteError();
toRemove.add(delivery);
tagGenerator.returnTag(delivery.getTag());
if (request != null && !request.isComplete()) {
request.onFailure(remoteError);
} else {
connection.getProvider().fireProviderException(remoteError);
}
} else if (state instanceof TransactionalState) {
LOG.info("State of delivery is Transacted: {}", state);
} else {
LOG.warn("Message send updated with unsupported state: {}", state);
}
}
pending.removeAll(toRemove);
}
@Override
protected void doOpen() {
JmsDestination destination = info.getDestination();
String destnationName = session.getQualifiedName(destination);
String sourceAddress = getProducerId().toString();
Source source = new Source();
source.setAddress(sourceAddress);
Target target = new Target();
target.setAddress(destnationName);
String senderName = sourceAddress + ":" + destnationName != null ? destnationName : "Anonymous";
endpoint = session.getProtonSession().sender(senderName);
endpoint.setSource(source);
endpoint.setTarget(target);
if (presettle) {
endpoint.setSenderSettleMode(SenderSettleMode.SETTLED);
} else {
endpoint.setSenderSettleMode(SenderSettleMode.UNSETTLED);
}
endpoint.setReceiverSettleMode(ReceiverSettleMode.FIRST);
}
@Override
protected void doClose() {
}
public AmqpSession getSession() {
return this.session;
}
public Sender getProtonSender() {
return this.endpoint;
}
@Override
public boolean isAnonymous() {
return false;
}
@Override
public void setPresettle(boolean presettle) {
this.presettle = presettle;
}
@Override
public boolean isPresettle() {
return this.presettle;
}
@Override
public String toString() {
return "AmqpFixedProducer { " + getProducerId() + " }";
}
private class PendingSend {
public JmsOutboundMessageDispatch envelope;
public AsyncResult<Void> request;
public PendingSend(JmsOutboundMessageDispatch envelope, AsyncResult<Void> request) {
this.envelope = envelope;
this.request = request;
}
}
}