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<String, Set<String>>(); 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 * @return The JMS selector for the specified {@code destination} 253 */ 254 public Set<String> getSelector(final String destination) { 255 return subSelectorCache.get(destination); 256 } 257 258 /** 259 * Persist the selector cache every {@code MAX_PERSIST_INTERVAL}ms. 260 * 261 * @see java.lang.Runnable#run() 262 */ 263 @Override 264 public void run() { 265 while (running) { 266 try { 267 Thread.sleep(persistInterval); 268 } catch (InterruptedException ex) { 269 } 270 271 persistCache(); 272 } 273 } 274 275 public boolean isSingleSelectorPerDestination() { 276 return singleSelectorPerDestination; 277 } 278 279 public void setSingleSelectorPerDestination(boolean singleSelectorPerDestination) { 280 this.singleSelectorPerDestination = singleSelectorPerDestination; 281 } 282 283 @SuppressWarnings("unchecked") 284 public Set<String> getSelectorsForDestination(String destinationName) { 285 if (subSelectorCache.containsKey(destinationName)) { 286 return new HashSet<String>(subSelectorCache.get(destinationName)); 287 } 288 289 return Collections.EMPTY_SET; 290 } 291 292 public long getPersistInterval() { 293 return persistInterval; 294 } 295 296 public void setPersistInterval(long persistInterval) { 297 this.persistInterval = persistInterval; 298 } 299 300 public boolean deleteSelectorForDestination(String destinationName, String selector) { 301 if (subSelectorCache.containsKey(destinationName)) { 302 Set<String> cachedSelectors = subSelectorCache.get(destinationName); 303 return cachedSelectors.remove(selector); 304 } 305 306 return false; 307 } 308 309 public boolean deleteAllSelectorsForDestination(String destinationName) { 310 if (subSelectorCache.containsKey(destinationName)) { 311 Set<String> cachedSelectors = subSelectorCache.get(destinationName); 312 cachedSelectors.clear(); 313 } 314 return true; 315 } 316 317 public boolean isIgnoreWildcardSelectors() { 318 return ignoreWildcardSelectors; 319 } 320 321 public void setIgnoreWildcardSelectors(boolean ignoreWildcardSelectors) { 322 this.ignoreWildcardSelectors = ignoreWildcardSelectors; 323 } 324 325 // find wildcards inside like operator arguments 326 static class WildcardFinder { 327 328 private static final Pattern LIKE_PATTERN=Pattern.compile( 329 "\\bLIKE\\s+'(?<like>([^']|'')+)'(\\s+ESCAPE\\s+'(?<escape>.)')?", 330 Pattern.CASE_INSENSITIVE); 331 332 private static final String REGEX_SPECIAL = ".+?*(){}[]\\-"; 333 334 private static String getLike(final Matcher matcher) { 335 return matcher.group("like"); 336 } 337 338 private static boolean hasLikeOperator(final Matcher matcher) { 339 return matcher.find(); 340 } 341 342 private static String getEscape(final Matcher matcher) { 343 String escapeChar = matcher.group("escape"); 344 if (escapeChar == null) { 345 return null; 346 } else if (REGEX_SPECIAL.contains(escapeChar)) { 347 escapeChar = "\\"+escapeChar; 348 } 349 return escapeChar; 350 } 351 352 private static boolean hasWildcardInCurrentMatch(final Matcher matcher) { 353 String wildcards = "[_%]"; 354 if (getEscape(matcher) != null) { 355 wildcards = "(^|[^" + getEscape(matcher) + "])" + wildcards; 356 } 357 return Pattern.compile(wildcards).matcher(getLike(matcher)).find(); 358 } 359 360 public static boolean hasWildcards(String selector) { 361 Matcher matcher = LIKE_PATTERN.matcher(selector); 362 363 while(hasLikeOperator(matcher)) { 364 if (hasWildcardInCurrentMatch(matcher)) { 365 return true; 366 } 367 } 368 return false; 369 } 370 } 371 372 private static class SubSelectorClassObjectInputStream extends ObjectInputStream { 373 374 public SubSelectorClassObjectInputStream(InputStream is) throws IOException { 375 super(is); 376 } 377 378 @Override 379 protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { 380 if (!(desc.getName().equals("java.lang.String") || desc.getName().startsWith("java.util."))) { 381 throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName()); 382 } 383 return super.resolveClass(desc); 384 } 385 } 386}