001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.plugin;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.io.ObjectInputStream;
024import java.io.ObjectOutputStream;
025import java.util.Collections;
026import java.util.HashSet;
027import java.util.Set;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.ConcurrentMap;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033import javax.management.JMException;
034import javax.management.ObjectName;
035
036import org.apache.activemq.advisory.AdvisorySupport;
037import org.apache.activemq.broker.Broker;
038import org.apache.activemq.broker.BrokerFilter;
039import org.apache.activemq.broker.BrokerService;
040import org.apache.activemq.broker.ConnectionContext;
041import org.apache.activemq.broker.jmx.AnnotatedMBean;
042import org.apache.activemq.broker.jmx.BrokerMBeanSupport;
043import org.apache.activemq.broker.jmx.VirtualDestinationSelectorCacheView;
044import org.apache.activemq.broker.region.Subscription;
045import org.apache.activemq.command.ConsumerInfo;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049/**
050 * A plugin which allows the caching of the selector from a subscription queue.
051 * <p/>
052 * This stops the build-up of unwanted messages, especially when consumers may
053 * disconnect from time to time when using virtual destinations.
054 * <p/>
055 * This is influenced by code snippets developed by Maciej Rakowicz
056 *
057 * Refer to:
058 * https://issues.apache.org/activemq/browse/AMQ-3004
059 * http://mail-archives.apache.org/mod_mbox/activemq-users/201011.mbox/%3C8A013711-2613-450A-A487-379E784AF1D6@homeaway.co.uk%3E
060 */
061public class SubQueueSelectorCacheBroker extends BrokerFilter implements Runnable {
062    private static final Logger LOG = LoggerFactory.getLogger(SubQueueSelectorCacheBroker.class);
063    public static final String MATCH_EVERYTHING = "TRUE";
064
065    /**
066     * The subscription's selector cache. We cache compiled expressions keyed
067     * by the target destination.
068     */
069    private ConcurrentMap<String, Set<String>> subSelectorCache = new ConcurrentHashMap<String, Set<String>>();
070
071    private final File persistFile;
072    private boolean singleSelectorPerDestination = false;
073    private boolean ignoreWildcardSelectors = false;
074    private ObjectName objectName;
075
076    private boolean running = true;
077    private final Thread persistThread;
078    private long persistInterval = MAX_PERSIST_INTERVAL;
079    public static final long MAX_PERSIST_INTERVAL = 600000;
080    private static final String SELECTOR_CACHE_PERSIST_THREAD_NAME = "SelectorCachePersistThread";
081
082    /**
083     * Constructor
084     */
085    public SubQueueSelectorCacheBroker(Broker next, final File persistFile) {
086        super(next);
087        this.persistFile = persistFile;
088        LOG.info("Using persisted selector cache from[{}]", persistFile);
089
090        readCache();
091
092        persistThread = new Thread(this, SELECTOR_CACHE_PERSIST_THREAD_NAME);
093        persistThread.start();
094        enableJmx();
095    }
096
097    private void enableJmx() {
098        BrokerService broker = getBrokerService();
099        if (broker.isUseJmx()) {
100            VirtualDestinationSelectorCacheView view = new VirtualDestinationSelectorCacheView(this);
101            try {
102                objectName = BrokerMBeanSupport.createVirtualDestinationSelectorCacheName(broker.getBrokerObjectName(), "plugin", "virtualDestinationCache");
103                LOG.trace("virtualDestinationCacheSelector mbean name; " + objectName.toString());
104                AnnotatedMBean.registerMBean(broker.getManagementContext(), view, objectName);
105            } catch (Exception e) {
106                LOG.warn("JMX is enabled, but when installing the VirtualDestinationSelectorCache, couldn't install the JMX mbeans. Continuing without installing the mbeans.");
107            }
108        }
109    }
110
111    @Override
112    public void stop() throws Exception {
113        running = false;
114        if (persistThread != null) {
115            persistThread.interrupt();
116            persistThread.join();
117        }
118        unregisterMBeans();
119    }
120
121    private void unregisterMBeans() {
122        BrokerService broker = getBrokerService();
123        if (broker.isUseJmx() && this.objectName != null) {
124            try {
125                broker.getManagementContext().unregisterMBean(objectName);
126            } catch (JMException e) {
127                LOG.warn("Trying uninstall VirtualDestinationSelectorCache; couldn't uninstall mbeans, continuting...");
128            }
129        }
130    }
131
132    @Override
133    public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
134                // don't track selectors for advisory topics, temp destinations or console
135                // related consumers
136                if (!AdvisorySupport.isAdvisoryTopic(info.getDestination()) && !info.getDestination().isTemporary()
137                                && !info.isBrowser()) {
138            String destinationName = info.getDestination().getQualifiedName();
139            LOG.debug("Caching consumer selector [{}] on  '{}'", info.getSelector(), destinationName);
140
141            String selector = info.getSelector() == null ? MATCH_EVERYTHING : info.getSelector();
142
143            if (!(ignoreWildcardSelectors && hasWildcards(selector))) {
144
145                Set<String> selectors = subSelectorCache.get(destinationName);
146                if (selectors == null) {
147                    selectors = Collections.synchronizedSet(new HashSet<String>());
148                } else if (singleSelectorPerDestination && !MATCH_EVERYTHING.equals(selector)) {
149                    // in this case, we allow only ONE selector. But we don't count the catch-all "null/TRUE" selector
150                    // here, we always allow that one. But only one true selector.
151                    boolean containsMatchEverything = selectors.contains(MATCH_EVERYTHING);
152                    selectors.clear();
153
154                    // put back the MATCH_EVERYTHING selector
155                    if (containsMatchEverything) {
156                        selectors.add(MATCH_EVERYTHING);
157                    }
158                }
159
160                LOG.debug("adding new selector: into cache " + selector);
161                selectors.add(selector);
162                LOG.debug("current selectors in cache: " + selectors);
163                subSelectorCache.put(destinationName, selectors);
164            }
165        }
166
167        return super.addConsumer(context, info);
168    }
169
170    static boolean hasWildcards(String selector) {
171        return WildcardFinder.hasWildcards(selector);
172    }
173
174    @Override
175    public void removeConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
176        if (!AdvisorySupport.isAdvisoryTopic(info.getDestination()) && !info.getDestination().isTemporary()) {
177            if (singleSelectorPerDestination) {
178                String destinationName = info.getDestination().getQualifiedName();
179                Set<String> selectors = subSelectorCache.get(destinationName);
180                if (info.getSelector() == null && selectors.size() > 1) {
181                    boolean removed = selectors.remove(MATCH_EVERYTHING);
182                    LOG.debug("A non-selector consumer has dropped. Removing the catchall matching pattern 'TRUE'. Successful? " + removed);
183                }
184            }
185
186        }
187        super.removeConsumer(context, info);
188    }
189
190    @SuppressWarnings("unchecked")
191    private void readCache() {
192        if (persistFile != null && persistFile.exists()) {
193            try {
194                try (FileInputStream fis = new FileInputStream(persistFile);) {
195                    ObjectInputStream in = new ObjectInputStream(fis);
196                    try {
197                        LOG.debug("Reading selector cache....");
198                        subSelectorCache = (ConcurrentHashMap<String, Set<String>>) in.readObject();
199
200                        if (LOG.isDebugEnabled()) {
201                            final StringBuilder sb = new StringBuilder();
202                            sb.append("Selector cache data loaded from: ").append(persistFile.getAbsolutePath()).append("\n");
203                            sb.append("The following entries were loaded from the cache file: \n");
204
205                            subSelectorCache.forEach((k,v) -> {
206                                sb.append("\t").append(k).append(": ").append(v).append("\n");
207                            });
208
209                            LOG.debug(sb.toString());
210                        }
211                    } catch (ClassNotFoundException ex) {
212                        LOG.error("Invalid selector cache data found. Please remove file.", ex);
213                    } finally {
214                        in.close();
215                    }
216                }
217            } catch (IOException ex) {
218                LOG.error("Unable to read persisted selector cache...it will be ignored!", ex);
219            }
220        }
221    }
222
223    /**
224     * Persist the selector cache.
225     */
226    private void persistCache() {
227        LOG.debug("Persisting selector cache....");
228        try {
229            FileOutputStream fos = new FileOutputStream(persistFile);
230            try {
231                ObjectOutputStream out = new ObjectOutputStream(fos);
232                try {
233                    out.writeObject(subSelectorCache);
234                } finally {
235                    out.flush();
236                    out.close();
237                }
238            } catch (IOException ex) {
239                LOG.error("Unable to persist selector cache", ex);
240            } finally {
241                fos.close();
242            }
243        } catch (IOException ex) {
244            LOG.error("Unable to access file[{}]", persistFile, ex);
245        }
246    }
247
248    /**
249     * @return The JMS selector for the specified {@code destination}
250     */
251    public Set<String> getSelector(final String destination) {
252        return subSelectorCache.get(destination);
253    }
254
255    /**
256     * Persist the selector cache every {@code MAX_PERSIST_INTERVAL}ms.
257     *
258     * @see java.lang.Runnable#run()
259     */
260    @Override
261    public void run() {
262        while (running) {
263            try {
264                Thread.sleep(persistInterval);
265            } catch (InterruptedException ex) {
266            }
267
268            persistCache();
269        }
270    }
271
272    public boolean isSingleSelectorPerDestination() {
273        return singleSelectorPerDestination;
274    }
275
276    public void setSingleSelectorPerDestination(boolean singleSelectorPerDestination) {
277        this.singleSelectorPerDestination = singleSelectorPerDestination;
278    }
279
280    @SuppressWarnings("unchecked")
281    public Set<String> getSelectorsForDestination(String destinationName) {
282        if (subSelectorCache.containsKey(destinationName)) {
283            return new HashSet<String>(subSelectorCache.get(destinationName));
284        }
285
286        return Collections.EMPTY_SET;
287    }
288
289    public long getPersistInterval() {
290        return persistInterval;
291    }
292
293    public void setPersistInterval(long persistInterval) {
294        this.persistInterval = persistInterval;
295    }
296
297    public boolean deleteSelectorForDestination(String destinationName, String selector) {
298        if (subSelectorCache.containsKey(destinationName)) {
299            Set<String> cachedSelectors = subSelectorCache.get(destinationName);
300            return cachedSelectors.remove(selector);
301        }
302
303        return false;
304    }
305
306    public boolean deleteAllSelectorsForDestination(String destinationName) {
307        if (subSelectorCache.containsKey(destinationName)) {
308            Set<String> cachedSelectors = subSelectorCache.get(destinationName);
309            cachedSelectors.clear();
310        }
311        return true;
312    }
313
314    public boolean isIgnoreWildcardSelectors() {
315        return ignoreWildcardSelectors;
316    }
317
318    public void setIgnoreWildcardSelectors(boolean ignoreWildcardSelectors) {
319        this.ignoreWildcardSelectors = ignoreWildcardSelectors;
320    }
321
322    // find wildcards inside like operator arguments
323    static class WildcardFinder {
324
325        private static final Pattern LIKE_PATTERN=Pattern.compile(
326                "\\bLIKE\\s+'(?<like>([^']|'')+)'(\\s+ESCAPE\\s+'(?<escape>.)')?",
327                Pattern.CASE_INSENSITIVE);
328
329        private static final String REGEX_SPECIAL = ".+?*(){}[]\\-";
330
331        private static String getLike(final Matcher matcher) {
332            return matcher.group("like");
333        }
334
335        private static boolean hasLikeOperator(final Matcher matcher) {
336            return matcher.find();
337        }
338
339        private static String getEscape(final Matcher matcher) {
340            String escapeChar = matcher.group("escape");
341            if (escapeChar == null) {
342                return null;
343            } else if (REGEX_SPECIAL.contains(escapeChar)) {
344                escapeChar = "\\"+escapeChar;
345            }
346            return escapeChar;
347        }
348
349        private static boolean hasWildcardInCurrentMatch(final Matcher matcher) {
350            String wildcards = "[_%]";
351            if (getEscape(matcher) != null) {
352                wildcards = "(^|[^" + getEscape(matcher) + "])" + wildcards;
353            }
354            return Pattern.compile(wildcards).matcher(getLike(matcher)).find();
355        }
356
357        public static boolean hasWildcards(String selector) {
358            Matcher matcher = LIKE_PATTERN.matcher(selector);
359
360            while(hasLikeOperator(matcher)) {
361                if (hasWildcardInCurrentMatch(matcher)) {
362                    return true;
363                }
364            }
365            return false;
366        }
367    }
368}