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