/**
* 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.cxf.ws.rm.soap;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TimerTask;
import java.util.TreeSet;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.stream.XMLStreamReader;
import org.w3c.dom.Node;
import org.apache.cxf.Bus;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.interceptor.InterceptorChain;
import org.apache.cxf.interceptor.InterceptorProvider;
import org.apache.cxf.io.CachedOutputStream;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.phase.PhaseManager;
import org.apache.cxf.staxutils.StaxSource;
import org.apache.cxf.ws.rm.DestinationSequence;
import org.apache.cxf.ws.rm.RMCaptureInInterceptor;
import org.apache.cxf.ws.rm.RMContextUtils;
import org.apache.cxf.ws.rm.RMManager;
import org.apache.cxf.ws.rm.RMMessageConstants;
import org.apache.cxf.ws.rm.RMProperties;
import org.apache.cxf.ws.rm.RedeliveryQueue;
import org.apache.cxf.ws.rm.RetryStatus;
import org.apache.cxf.ws.rm.manager.RetryPolicyType;
import org.apache.cxf.ws.rm.persistence.RMStore;
import org.apache.cxf.ws.rm.v200702.Identifier;
import org.apache.cxf.ws.rm.v200702.SequenceType;
/**
*
*/
public class RedeliveryQueueImpl implements RedeliveryQueue {
private static final Logger LOG = LogUtils.getL7dLogger(RedeliveryQueueImpl.class);
private Map<String, List<RedeliverCandidate>> candidates =
new HashMap<String, List<RedeliverCandidate>>();
private Map<String, List<RedeliverCandidate>> suspendedCandidates =
new HashMap<String, List<RedeliverCandidate>>();
private RMManager manager;
private int undeliveredCount;
public RedeliveryQueueImpl(RMManager m) {
manager = m;
}
public RMManager getManager() {
return manager;
}
public void setManager(RMManager m) {
manager = m;
}
public void addUndelivered(Message message) {
cacheUndelivered(message);
}
/**
* @param seq the sequence under consideration
* @return the number of undelivered messages for that sequence
*/
public synchronized int countUndelivered(DestinationSequence seq) {
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq);
return sequenceCandidates == null ? 0 : sequenceCandidates.size();
}
public int countUndelivered() {
return undeliveredCount;
}
public boolean isEmpty() {
return 0 == getUndelivered().size();
}
public void purgeAll(DestinationSequence seq) {
Collection<Long> purged = new ArrayList<>();
synchronized (this) {
LOG.fine("Start purging redeliver candidates.");
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq);
if (null != sequenceCandidates) {
for (int i = sequenceCandidates.size() - 1; i >= 0; i--) {
RedeliverCandidate candidate = sequenceCandidates.get(i);
long m = candidate.getNumber();
sequenceCandidates.remove(i);
candidate.resolved();
undeliveredCount--;
purged.add(m);
}
if (sequenceCandidates.isEmpty()) {
candidates.remove(seq.getIdentifier().getValue());
}
}
LOG.fine("Completed purging redeliver candidates.");
}
if (!purged.isEmpty()) {
RMStore store = manager.getStore();
if (null != store) {
store.removeMessages(seq.getIdentifier(), purged, false);
}
}
}
public List<Long> getUndeliveredMessageNumbers(DestinationSequence seq) {
List<Long> undelivered = new ArrayList<>();
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq);
if (null != sequenceCandidates) {
for (int i = 0; i < sequenceCandidates.size(); i++) {
RedeliverCandidate candidate = sequenceCandidates.get(i);
RMProperties properties = RMContextUtils.retrieveRMProperties(candidate.getMessage(),
false);
SequenceType st = properties.getSequence();
undelivered.add(st.getMessageNumber());
}
}
return undelivered;
}
/**
* @param seq the sequence under consideration
* @return the list of resend candidates for that sequence
* @pre called with mutex held
*/
protected List<RedeliverCandidate> getSequenceCandidates(DestinationSequence seq) {
return getSequenceCandidates(seq.getIdentifier().getValue());
}
/**
* @param key the sequence identifier under consideration
* @return the list of resend candidates for that sequence
* @pre called with mutex held
*/
protected List<RedeliverCandidate> getSequenceCandidates(String key) {
List<RedeliverCandidate> sc = candidates.get(key);
if (null == sc) {
sc = suspendedCandidates.get(key);
}
return sc;
}
/**
* @param key the sequence identifier under consideration
* @return true if the sequence is currently suspended; false otherwise
* @pre called with mutex held
*/
protected boolean isSequenceSuspended(String key) {
return suspendedCandidates.containsKey(key);
}
public RetryStatus getRedeliveryStatus(DestinationSequence seq, long num) {
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq);
if (null != sequenceCandidates) {
for (int i = 0; i < sequenceCandidates.size(); i++) {
RedeliverCandidate candidate = sequenceCandidates.get(i);
RMProperties properties = RMContextUtils.retrieveRMProperties(candidate.getMessage(),
false);
SequenceType st = properties.getSequence();
if (num == st.getMessageNumber()) {
return candidate;
}
}
}
return null;
}
public Map<Long, RetryStatus> getRedeliveryStatuses(DestinationSequence seq) {
Map<Long, RetryStatus> cp = new HashMap<>();
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq);
if (null != sequenceCandidates) {
for (int i = 0; i < sequenceCandidates.size(); i++) {
RedeliverCandidate candidate = sequenceCandidates.get(i);
RMProperties properties = RMContextUtils.retrieveRMProperties(candidate.getMessage(),
false);
SequenceType st = properties.getSequence();
cp.put(st.getMessageNumber(), candidate);
}
}
return cp;
}
public void start() {
// TODO Auto-generated method stub
}
public void stop(DestinationSequence seq) {
synchronized (this) {
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(seq);
if (null != sequenceCandidates) {
for (int i = sequenceCandidates.size() - 1; i >= 0; i--) {
RedeliverCandidate candidate = sequenceCandidates.get(i);
candidate.cancel();
}
LOG.log(Level.FINE, "Cancelled redeliveriss for sequence {0}.",
seq.getIdentifier().getValue());
}
}
}
public void suspend(DestinationSequence seq) {
synchronized (this) {
String key = seq.getIdentifier().getValue();
List<RedeliverCandidate> sequenceCandidates = candidates.remove(key);
if (null != sequenceCandidates) {
for (int i = sequenceCandidates.size() - 1; i >= 0; i--) {
RedeliverCandidate candidate = sequenceCandidates.get(i);
candidate.suspend();
}
suspendedCandidates.put(key, sequenceCandidates);
LOG.log(Level.FINE, "Suspended redeliveris for sequence {0}.", key);
}
}
}
public void resume(DestinationSequence seq) {
synchronized (this) {
String key = seq.getIdentifier().getValue();
List<RedeliverCandidate> sequenceCandidates = suspendedCandidates.remove(key);
if (null != sequenceCandidates) {
for (int i = 0; i < sequenceCandidates.size(); i++) {
RedeliverCandidate candidate = sequenceCandidates.get(i);
candidate.resume();
}
candidates.put(key, sequenceCandidates);
LOG.log(Level.FINE, "Resumed redeliveries for sequence {0}.", key);
}
}
}
/**
* Accepts a new resend candidate.
*
* @param ctx the message context.
* @return ResendCandidate
*/
protected RedeliverCandidate cacheUndelivered(Message message) {
RMProperties rmps = RMContextUtils.retrieveRMProperties(message, false);
SequenceType st = rmps.getSequence();
Identifier sid = st.getIdentifier();
String key = sid.getValue();
RedeliverCandidate candidate = null;
synchronized (this) {
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(key);
if (null == sequenceCandidates) {
sequenceCandidates = new ArrayList<>();
candidates.put(key, sequenceCandidates);
}
candidate = getRedeliverCandidate(st, sequenceCandidates);
if (candidate == null) {
candidate = new RedeliverCandidate(message);
if (isSequenceSuspended(key)) {
candidate.suspend();
}
sequenceCandidates.add(candidate);
undeliveredCount++;
}
}
LOG.fine("Cached undelivered message.");
return candidate;
}
private RedeliverCandidate getRedeliverCandidate(SequenceType st, List<RedeliverCandidate> rcs) {
// assume the size of candidates to be relatively small; otherwise we should use message numbers as keys
for (RedeliverCandidate rc : rcs) {
if (st.getMessageNumber() == rc.getNumber()) {
return rc;
}
}
return null;
}
protected void purgeDelivered(RedeliverCandidate candidate) {
RMProperties rmps = RMContextUtils.retrieveRMProperties(candidate.getMessage(), false);
SequenceType st = rmps.getSequence();
Identifier sid = st.getIdentifier();
String key = sid.getValue();
synchronized (this) {
List<RedeliverCandidate> sequenceCandidates = getSequenceCandidates(key);
if (null != sequenceCandidates) {
// TODO use a constant op instead of this inefficient linear op
sequenceCandidates.remove(candidate);
undeliveredCount--;
}
if (sequenceCandidates.isEmpty()) {
candidates.remove(sid.getValue());
}
}
LOG.fine("Purged delivered message.");
}
/**
* @return a map relating sequence ID to a lists of un-acknowledged messages
* for that sequence
*/
protected Map<String, List<RedeliverCandidate>> getUndelivered() {
return candidates;
}
private static InterceptorChain getRedeliveryInterceptorChain(Message m, String phase) {
Exchange exchange = m.getExchange();
Endpoint ep = exchange.getEndpoint();
Bus bus = exchange.getBus();
PhaseManager pm = bus.getExtension(PhaseManager.class);
SortedSet<Phase> phases = new TreeSet<Phase>(pm.getInPhases());
for (Iterator<Phase> it = phases.iterator(); it.hasNext();) {
Phase p = it.next();
if (phase.equals(p.getName())) {
break;
}
it.remove();
}
PhaseInterceptorChain chain = new PhaseInterceptorChain(phases);
List<Interceptor<? extends Message>> il = ep.getInInterceptors();
addInterceptors(chain, il);
il = ep.getService().getInInterceptors();
addInterceptors(chain, il);
il = ep.getBinding().getInInterceptors();
addInterceptors(chain, il);
il = bus.getInInterceptors();
addInterceptors(chain, il);
if (ep.getService().getDataBinding() instanceof InterceptorProvider) {
il = ((InterceptorProvider)ep.getService().getDataBinding()).getInInterceptors();
addInterceptors(chain, il);
}
return chain;
}
private static void addInterceptors(PhaseInterceptorChain chain,
List<Interceptor<? extends Message>> il) {
for (Interceptor<? extends Message> i : il) {
final String iname = i.getClass().getSimpleName();
if ("OneWayProcessorInterceptor".equals(iname)
|| "MAPAggregatorImpl".equals(iname)
|| "RMInInterceptor".equals(iname)) {
continue;
}
chain.add(i);
}
}
//TODO refactor this class to unify its functionality with that of ResendCandidate
protected class RedeliverCandidate implements Runnable, RetryStatus {
private Message message;
private long number;
private Date next;
private TimerTask nextTask;
private int retries;
private int maxRetries;
private long nextInterval;
private long backoff;
private boolean pending;
private boolean suspended;
protected RedeliverCandidate(Message m) {
message = m;
if (message instanceof SoapMessage) {
// remove old message headers like WSS headers
((SoapMessage)message).getHeaders().clear();
}
RetryPolicyType rmrp = null != manager.getDestinationPolicy()
? manager.getDestinationPolicy().getRetryPolicy() : null;
long baseRedeliveryInterval = Long.parseLong(DEFAULT_BASE_REDELIVERY_INTERVAL);
if (null != rmrp && rmrp.getInterval() > 0L) {
baseRedeliveryInterval = rmrp.getInterval();
}
backoff = RedeliveryQueue.DEFAULT_EXPONENTIAL_BACKOFF;
next = new Date(System.currentTimeMillis() + baseRedeliveryInterval);
nextInterval = baseRedeliveryInterval * backoff;
maxRetries = null != rmrp ? rmrp.getMaxRetries() : 0;
RMProperties rmprops = RMContextUtils.retrieveRMProperties(message, false);
if (null != rmprops) {
number = rmprops.getSequence().getMessageNumber();
}
if (null != manager.getTimer() && maxRetries != 0) {
schedule();
}
}
/**
* Initiate redelivery asynchronsly.
*
*/
protected void initiate() {
pending = true;
Endpoint ep = message.getExchange().get(Endpoint.class);
Executor executor = ep.getExecutor();
if (null == executor) {
executor = ep.getService().getExecutor();
LOG.log(Level.FINE, "Using service executor {0}", executor.getClass().getName());
} else {
LOG.log(Level.FINE, "Using endpoint executor {0}", executor.getClass().getName());
}
try {
executor.execute(this);
} catch (RejectedExecutionException ex) {
LOG.log(Level.SEVERE, "RESEND_INITIATION_FAILED_MSG", ex);
}
}
public void run() {
try {
if (isPending()) {
// redeliver
redeliver();
purgeDelivered(this);
resolved();
}
} catch (Exception ex) {
LOG.log(Level.WARNING, "redelivery failed", ex);
} finally {
attempted();
}
}
private void redeliver() throws Exception {
LOG.log(Level.INFO, "Redelivering ... for " + (1 + retries));
String restartingPhase;
if (message.getContent(Exception.class) != null) {
message.removeContent(Exception.class);
message.getExchange().put(Exception.class, null);
// clean-up message for redelivery
closeStreamResources();
message.removeContent(Node.class);
}
InputStream is = null;
CachedOutputStream cos = (CachedOutputStream)message.get(RMMessageConstants.SAVED_CONTENT);
is = cos.getInputStream();
message.setContent(InputStream.class, is);
message = message.getExchange().getEndpoint().getBinding().createMessage(message);
restartingPhase = Phase.POST_STREAM;
// skip some interceptor chain phases for redelivery
InterceptorChain chain = getRedeliveryInterceptorChain(message, restartingPhase);
ListIterator<Interceptor<? extends Message>> iterator = chain.getIterator();
while (iterator.hasNext()) {
Interceptor<? extends Message> incept = iterator.next();
if (incept.getClass().getName().equals(RMCaptureInInterceptor.class.getName())) {
chain.remove(incept);
}
}
message.getExchange().setInMessage(message);
message.setInterceptorChain(chain);
chain.doIntercept(message);
Exception ex = message.getContent(Exception.class);
if (null != ex) {
throw ex;
}
}
public long getNumber() {
return number;
}
public Date getNext() {
return next;
}
public Date getPrevious() {
if (retries > 0) {
return new Date(next.getTime() - nextInterval / backoff);
}
return null;
}
public int getRetries() {
return retries;
}
public int getMaxRetries() {
return maxRetries;
}
public long getNextInterval() {
return nextInterval;
}
public long getBackoff() {
return backoff;
}
public boolean isPending() {
return pending;
}
public boolean isSuspended() {
return suspended;
}
/**
* the message has been delivered to the application
*/
protected synchronized void resolved() {
pending = false;
next = null;
if (null != nextTask) {
nextTask.cancel();
}
}
/**
* Cancel further redelivery (although not successfully delivered).
*/
protected void cancel() {
if (null != nextTask) {
nextTask.cancel();
closeStreamResources();
releaseSavedMessage();
}
}
protected void suspend() {
suspended = true;
pending = false;
//TODO release the message and later reload it upon resume
//cancel();
if (null != nextTask) {
nextTask.cancel();
}
}
protected void resume() {
suspended = false;
next = new Date(System.currentTimeMillis());
attempted();
}
private void releaseSavedMessage() {
CachedOutputStream saved = (CachedOutputStream)message.remove(RMMessageConstants.SAVED_CONTENT);
if (saved != null) {
saved.releaseTempFileHold();
try {
saved.close();
} catch (IOException e) {
// ignore
}
}
// Any unclosed resources must be closed to release the temp files.
Closeable closeable = (Closeable)message.get(RMMessageConstants.ATTACHMENTS_CLOSEABLE);
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
// ignore
}
}
}
/*
* Close all stream-like resources stored in the message
*/
private void closeStreamResources() {
InputStream oin = message.getContent(InputStream.class);
if (oin != null) {
try {
oin.close();
} catch (Exception e) {
// ignore
}
message.removeContent(InputStream.class);
}
XMLStreamReader oreader = message.getContent(XMLStreamReader.class);
if (oreader != null) {
try {
oreader.close();
} catch (Exception e) {
// ignore
}
message.removeContent(XMLStreamReader.class);
}
List<?> olist = message.getContent(List.class);
if (olist != null && olist.size() == 1) {
Object o = olist.get(0);
if (o instanceof XMLStreamReader) {
oreader = (XMLStreamReader)o;
} else if (o instanceof StaxSource) {
oreader = ((StaxSource)o).getXMLStreamReader();
}
if (oreader != null) {
try {
oreader.close();
} catch (Exception e) {
// ignore
}
}
message.removeContent(List.class);
}
}
/**
* @return associated message context
*/
protected Message getMessage() {
return message;
}
/**
* A resend has been attempted. Schedule the next attempt.
*/
protected synchronized void attempted() {
pending = false;
retries++;
if (null != next && maxRetries != retries) {
next = new Date(next.getTime() + nextInterval);
nextInterval *= backoff;
schedule();
}
}
protected final synchronized void schedule() {
if (null == manager.getTimer()) {
return;
}
class RedeliverTask extends TimerTask {
RedeliverCandidate candidate;
RedeliverTask(RedeliverCandidate c) {
candidate = c;
}
@Override
public void run() {
if (!candidate.isPending()) {
candidate.initiate();
}
}
}
nextTask = new RedeliverTask(this);
try {
manager.getTimer().schedule(nextTask, next);
} catch (IllegalStateException ex) {
LOG.log(Level.WARNING, "SCHEDULE_RESEND_FAILED_MSG", ex);
}
}
}
}