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                        subSelectorCache = (ConcurrentHashMap<String, Set<String>>) in.readObject();
198                    } catch (ClassNotFoundException ex) {
199                        LOG.error("Invalid selector cache data found. Please remove file.", ex);
200                    } finally {
201                        in.close();
202                    }
203                }
204            } catch (IOException ex) {
205                LOG.error("Unable to read persisted selector cache...it will be ignored!", ex);
206            }
207        }
208    }
209
210    /**
211     * Persist the selector cache.
212     */
213    private void persistCache() {
214        LOG.debug("Persisting selector cache....");
215        try {
216            FileOutputStream fos = new FileOutputStream(persistFile);
217            try {
218                ObjectOutputStream out = new ObjectOutputStream(fos);
219                try {
220                    out.writeObject(subSelectorCache);
221                } finally {
222                    out.flush();
223                    out.close();
224                }
225            } catch (IOException ex) {
226                LOG.error("Unable to persist selector cache", ex);
227            } finally {
228                fos.close();
229            }
230        } catch (IOException ex) {
231            LOG.error("Unable to access file[{}]", persistFile, ex);
232        }
233    }
234
235    /**
236     * @return The JMS selector for the specified {@code destination}
237     */
238    public Set<String> getSelector(final String destination) {
239        return subSelectorCache.get(destination);
240    }
241
242    /**
243     * Persist the selector cache every {@code MAX_PERSIST_INTERVAL}ms.
244     *
245     * @see java.lang.Runnable#run()
246     */
247    @Override
248    public void run() {
249        while (running) {
250            try {
251                Thread.sleep(persistInterval);
252            } catch (InterruptedException ex) {
253            }
254
255            persistCache();
256        }
257    }
258
259    public boolean isSingleSelectorPerDestination() {
260        return singleSelectorPerDestination;
261    }
262
263    public void setSingleSelectorPerDestination(boolean singleSelectorPerDestination) {
264        this.singleSelectorPerDestination = singleSelectorPerDestination;
265    }
266
267    @SuppressWarnings("unchecked")
268    public Set<String> getSelectorsForDestination(String destinationName) {
269        if (subSelectorCache.containsKey(destinationName)) {
270            return new HashSet<String>(subSelectorCache.get(destinationName));
271        }
272
273        return Collections.EMPTY_SET;
274    }
275
276    public long getPersistInterval() {
277        return persistInterval;
278    }
279
280    public void setPersistInterval(long persistInterval) {
281        this.persistInterval = persistInterval;
282    }
283
284    public boolean deleteSelectorForDestination(String destinationName, String selector) {
285        if (subSelectorCache.containsKey(destinationName)) {
286            Set<String> cachedSelectors = subSelectorCache.get(destinationName);
287            return cachedSelectors.remove(selector);
288        }
289
290        return false;
291    }
292
293    public boolean deleteAllSelectorsForDestination(String destinationName) {
294        if (subSelectorCache.containsKey(destinationName)) {
295            Set<String> cachedSelectors = subSelectorCache.get(destinationName);
296            cachedSelectors.clear();
297        }
298        return true;
299    }
300
301    public boolean isIgnoreWildcardSelectors() {
302        return ignoreWildcardSelectors;
303    }
304
305    public void setIgnoreWildcardSelectors(boolean ignoreWildcardSelectors) {
306        this.ignoreWildcardSelectors = ignoreWildcardSelectors;
307    }
308
309    // find wildcards inside like operator arguments
310    static class WildcardFinder {
311
312        private static final Pattern LIKE_PATTERN=Pattern.compile(
313                "\\bLIKE\\s+'(?<like>([^']|'')+)'(\\s+ESCAPE\\s+'(?<escape>.)')?",
314                Pattern.CASE_INSENSITIVE);
315
316        private static final String REGEX_SPECIAL = ".+?*(){}[]\\-";
317
318        private static String getLike(final Matcher matcher) {
319            return matcher.group("like");
320        }
321
322        private static boolean hasLikeOperator(final Matcher matcher) {
323            return matcher.find();
324        }
325
326        private static String getEscape(final Matcher matcher) {
327            String escapeChar = matcher.group("escape");
328            if (escapeChar == null) {
329                return null;
330            } else if (REGEX_SPECIAL.contains(escapeChar)) {
331                escapeChar = "\\"+escapeChar;
332            }
333            return escapeChar;
334        }
335
336        private static boolean hasWildcardInCurrentMatch(final Matcher matcher) {
337            String wildcards = "[_%]";
338            if (getEscape(matcher) != null) {
339                wildcards = "(^|[^" + getEscape(matcher) + "])" + wildcards;
340            }
341            return Pattern.compile(wildcards).matcher(getLike(matcher)).find();
342        }
343
344        public static boolean hasWildcards(String selector) {
345            Matcher matcher = LIKE_PATTERN.matcher(selector);
346
347            while(hasLikeOperator(matcher)) {
348                if (hasWildcardInCurrentMatch(matcher)) {
349                    return true;
350                }
351            }
352            return false;
353        }
354    }
355}