/* * Copyright 2015 the original author or authors. * * Licensed 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.springframework.xd.dirt.integration.bus.rabbit; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.xd.dirt.integration.bus.BusCleaner; import org.springframework.xd.dirt.integration.bus.RabbitManagementUtils; import org.springframework.xd.dirt.integration.bus.BusUtils; import org.springframework.xd.dirt.integration.bus.MessageBusSupport; import org.springframework.xd.dirt.integration.bus.RabbitAdminException; import org.springframework.xd.dirt.plugins.AbstractJobPlugin; import org.springframework.xd.dirt.plugins.job.JobEventsListenerPlugin; /** * Implementation of {@link BusCleaner} for the {@code RabbitMessageBus}. * * @author Gary Russell * @since 1.2 */ public class RabbitBusCleaner implements BusCleaner { private final static Logger logger = LoggerFactory.getLogger(RabbitBusCleaner.class); @Override public Map<String, List<String>> clean(String entity, boolean isJob) { return clean("http://localhost:15672", "guest", "guest", "/", "xdbus.", entity, isJob); } public Map<String, List<String>> clean(String adminUri, String user, String pw, String vhost, String busPrefix, String entity, boolean isJob) { return doClean( adminUri == null ? "http://localhost:15672" : adminUri, user == null ? "guest" : user, pw == null ? "guest" : pw, vhost == null ? "/" : vhost, busPrefix == null ? "xdbus." : busPrefix, entity, isJob); } private Map<String, List<String>> doClean(String adminUri, String user, String pw, String vhost, String busPrefix, String entity, boolean isJob) { RestTemplate restTemplate = RabbitManagementUtils.buildRestTemplate(adminUri, user, pw); List<String> removedQueues = isJob ? findJobQueues(adminUri, vhost, busPrefix, entity, restTemplate) : findStreamQueues(adminUri, vhost, busPrefix, entity, restTemplate); ExchangeCandidateCallback callback; if (isJob) { String pattern; if (entity.endsWith("*")) { pattern = entity.substring(0, entity.length() - 1) + "[^.]*"; } else { pattern = entity; } Collection<String> exchangeNames = JobEventsListenerPlugin.getEventListenerChannels(pattern).values(); final Set<Pattern> jobExchanges = new HashSet<>(); for (String exchange : exchangeNames) { jobExchanges.add(Pattern.compile(MessageBusSupport.applyPrefix(busPrefix, MessageBusSupport.applyPubSub(exchange)))); } jobExchanges.add(Pattern.compile(MessageBusSupport.applyPrefix(busPrefix, MessageBusSupport.applyPubSub( JobEventsListenerPlugin.getEventListenerChannelName(pattern))))); callback = new ExchangeCandidateCallback() { @Override public boolean isCandidate(String exchangeName) { for (Pattern pattern : jobExchanges) { Matcher matcher = pattern.matcher(exchangeName); if (matcher.matches()) { return true; } } return false; } }; } else { final String tapPrefix = adjustPrefix(MessageBusSupport.applyPrefix(busPrefix, MessageBusSupport.applyPubSub(BusUtils.constructTapPrefix(entity)))); callback = new ExchangeCandidateCallback() { @Override public boolean isCandidate(String exchangeName) { return exchangeName.startsWith(tapPrefix); } }; } List<String> removedExchanges = findExchanges(adminUri, vhost, busPrefix, entity, restTemplate, callback); // Delete the queues in reverse order to enable re-running after a partial success. // The queue search above starts with 0 and terminates on a not found. for (int i = removedQueues.size() - 1; i >= 0; i--) { String queueName = removedQueues.get(i); URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api") .pathSegment("queues", "{vhost}", "{stream}") .buildAndExpand(vhost, queueName).encode().toUri(); restTemplate.delete(uri); if (logger.isDebugEnabled()) { logger.debug("deleted queue: " + queueName); } } Map<String, List<String>> results = new HashMap<>(); if (removedQueues.size() > 0) { results.put("queues", removedQueues); } // Fanout exchanges for taps for (String exchange : removedExchanges) { URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api") .pathSegment("exchanges", "{vhost}", "{name}") .buildAndExpand(vhost, exchange).encode().toUri(); restTemplate.delete(uri); if (logger.isDebugEnabled()) { logger.debug("deleted exchange: " + exchange); } } if (removedExchanges.size() > 0) { results.put("exchanges", removedExchanges); } return results; } private List<String> findStreamQueues(String adminUri, String vhost, String busPrefix, String stream, RestTemplate restTemplate) { String queueNamePrefix = adjustPrefix(MessageBusSupport.applyPrefix(busPrefix, stream)); List<Map<String, Object>> queues = listAllQueues(adminUri, vhost, restTemplate); List<String> removedQueues = new ArrayList<>(); for (Map<String, Object> queue : queues) { String queueName = (String) queue.get("name"); if (queueName.startsWith(queueNamePrefix)) { checkNoConsumers(queueName, queue); removedQueues.add(queueName); } } return removedQueues; } private List<String> findJobQueues(String adminUri, String vhost, String busPrefix, String job, RestTemplate restTemplate) { List<String> removedQueues = new ArrayList<>(); String jobQueueName = MessageBusSupport.applyPrefix(busPrefix, AbstractJobPlugin.getJobChannelName(job)); String jobRequestsQueuePrefix = adjustPrefix(MessageBusSupport.applyPrefix(busPrefix, AbstractJobPlugin.getJobChannelName(job))); List<Map<String, Object>> queues = listAllQueues(adminUri, vhost, restTemplate); for (Map<String, Object> queue : queues) { String queueName = (String) queue.get("name"); if (job.endsWith("*")) { if (queueName.startsWith(jobQueueName.substring(0, jobQueueName.length() - 1))) { checkNoConsumers(queueName, queue); removedQueues.add(queueName); } } else { if (queueName.equals(jobQueueName)) { checkNoConsumers(queueName, queue); removedQueues.add(queueName); } else if (queueName.startsWith(jobRequestsQueuePrefix) && queueName.endsWith(MessageBusSupport.applyRequests(""))) { checkNoConsumers(queueName, queue); removedQueues.add(queueName); } } } return removedQueues; } private List<Map<String, Object>> listAllQueues(String adminUri, String vhost, RestTemplate restTemplate) { URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api") .pathSegment("queues", "{vhost}") .buildAndExpand(vhost).encode().toUri(); @SuppressWarnings("unchecked") List<Map<String, Object>> queues = restTemplate.getForObject(uri, List.class); return queues; } private String adjustPrefix(String prefix) { if (prefix.endsWith("*")) { return prefix.substring(0, prefix.length() - 1); } else { return prefix + BusUtils.GROUP_INDEX_DELIMITER; } } private void checkNoConsumers(String queueName, Map<String, Object> queue) { if (!queue.get("consumers").equals(Integer.valueOf(0))) { throw new RabbitAdminException("Queue " + queueName + " is in use"); } } @SuppressWarnings("unchecked") private List<String> findExchanges(String adminUri, String vhost, String busPrefix, String entity, RestTemplate restTemplate, ExchangeCandidateCallback callback) { List<String> removedExchanges = new ArrayList<>(); URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api") .pathSegment("exchanges", "{vhost}") .buildAndExpand(vhost).encode().toUri(); List<Map<String, Object>> exchanges = restTemplate.getForObject(uri, List.class); for (Map<String, Object> exchange : exchanges) { String exchangeName = (String) exchange.get("name"); if (callback.isCandidate(exchangeName)) { uri = UriComponentsBuilder.fromUriString(adminUri + "/api") .pathSegment("exchanges", "{vhost}", "{name}", "bindings", "source") .buildAndExpand(vhost, exchangeName).encode().toUri(); List<Map<String, Object>> bindings = restTemplate.getForObject(uri, List.class); if (bindings.size() == 0) { uri = UriComponentsBuilder.fromUriString(adminUri + "/api") .pathSegment("exchanges", "{vhost}", "{name}", "bindings", "destination") .buildAndExpand(vhost, exchangeName).encode().toUri(); bindings = restTemplate.getForObject(uri, List.class); if (bindings.size() == 0) { removedExchanges.add((String) exchange.get("name")); } else { throw new RabbitAdminException("Cannot delete exchange " + exchangeName + "; it is a destination: " + bindings); } } else { throw new RabbitAdminException("Cannot delete exchange " + exchangeName + "; it has bindings: " + bindings); } } } return removedExchanges; } private interface ExchangeCandidateCallback { boolean isCandidate(String exchangeName); } }