/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.activemq.artemis.core.settings.impl; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.settings.HierarchicalRepository; import org.apache.activemq.artemis.core.settings.HierarchicalRepositoryChangeListener; import org.apache.activemq.artemis.core.settings.Mergeable; import org.jboss.logging.Logger; /** * allows objects to be mapped against a regex pattern and held in order in a list */ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T> { private static final Logger logger = Logger.getLogger(HierarchicalObjectRepository.class); private boolean listenersEnabled = true; /** * The default Match to fall back to */ private T defaultmatch; /** * all the matches */ private final Map<String, Match<T>> matches = new HashMap<>(); /** * Certain values cannot be removed after installed. * This is because we read a few records from the main config. * JBoss AS deployer may remove them on undeploy, while we don't want to accept that since * this could cause issues on shutdown. * Notice you can still change these values. You just can't remove them. */ private final Set<String> immutables = new HashSet<>(); /** * a regex comparator */ private final MatchComparator matchComparator = new MatchComparator(); /** * a cache */ private final Map<String, T> cache = new ConcurrentHashMap<>(); /** * Need a lock instead of using multiple {@link ConcurrentHashMap}s. * <p> * We could have a race between the state of {@link #matches} and {@link #cache}: * <p> * Thread1: calls {@link #addMatch(String, T)}: i. cleans cache; ii. adds match to Map.<br> * Thread2: could add an (out-dated) entry to the cache between 'i. clean cache' and 'ii. add * match to Map'. * <p> * The lock is OK with regards to performance because we can search the cache before entering the * lock. * <p> * The lock is required for the 'add match to cache' part. */ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false); /** * any registered listeners, these get fired on changes to the repository */ private final ArrayList<HierarchicalRepositoryChangeListener> listeners = new ArrayList<>(); @Override public void disableListeners() { lock.writeLock().lock(); try { this.listenersEnabled = false; } finally { lock.writeLock().unlock(); } } @Override public void enableListeners() { lock.writeLock().lock(); try { this.listenersEnabled = true; } finally { lock.writeLock().unlock(); } onChange(); } @Override public void addMatch(final String match, final T value) { addMatch(match, value, false); } @Override public List<T> values() { lock.readLock().lock(); try { ArrayList<T> values = new ArrayList<>(matches.size()); for (Match<T> matchValue : matches.values()) { values.add(matchValue.getValue()); } return values; } finally { lock.readLock().unlock(); } } /** * Add a new match to the repository * * @param match The regex to use to match against * @param value the value to hold against the match */ @Override public void addMatch(final String match, final T value, final boolean immutableMatch) { addMatch(match, value, immutableMatch, true); } private void addMatch(final String match, final T value, final boolean immutableMatch, boolean notifyListeners) { lock.writeLock().lock(); try { clearCache(); if (immutableMatch) { immutables.add(match); } Match.verify(match); Match<T> match1 = new Match<>(match); match1.setValue(value); matches.put(match, match1); } finally { lock.writeLock().unlock(); } // Calling the onChange outside of the wrieLock as some listeners may be doing reads on the matches if (notifyListeners) { onChange(); } } @Override public int getCacheSize() { return cache.size(); } /** * return the value held against the nearest match * * @param match the match to look for * @return the value */ @Override public T getMatch(final String match) { T cacheResult = cache.get(match); if (cacheResult != null) { return cacheResult; } lock.readLock().lock(); try { T actualMatch; Map<String, Match<T>> possibleMatches = getPossibleMatches(match); Collection<Match<T>> orderedMatches = sort(possibleMatches); actualMatch = merge(orderedMatches); T value = actualMatch != null ? actualMatch : defaultmatch; if (value != null) { cache.put(match, value); } return value; } finally { lock.readLock().unlock(); } } /** * merge all the possible matches, if the values implement Mergeable then a full merge is done * * @param orderedMatches * @return */ private T merge(final Collection<Match<T>> orderedMatches) { T actualMatch = null; for (Match<T> match : orderedMatches) { if (actualMatch == null || !Mergeable.class.isAssignableFrom(actualMatch.getClass())) { actualMatch = match.getValue(); if (!Mergeable.class.isAssignableFrom(actualMatch.getClass())) { break; } } else { ((Mergeable) actualMatch).merge(match.getValue()); } } return actualMatch; } /** * Sort the matches according to their precedence (that is, according to the precedence of their * keys). * * @param possibleMatches * @return */ private List<Match<T>> sort(final Map<String, Match<T>> possibleMatches) { List<String> keys = new ArrayList<>(possibleMatches.keySet()); Collections.sort(keys, matchComparator); List<Match<T>> matches1 = new ArrayList<>(possibleMatches.size()); for (String key : keys) { matches1.add(possibleMatches.get(key)); } return matches1; } /** * remove a match from the repository * * @param match the match to remove */ @Override public void removeMatch(final String match) { lock.writeLock().lock(); try { boolean isImmutable = immutables.contains(match); if (isImmutable) { logger.debug("Cannot remove match " + match + " since it came from a main config"); } else { /** * clear the cache before removing the match. This will force any thread at * {@link #getMatch(String)} to get the lock to recompute. */ clearCache(); matches.remove(match); onChange(); } } finally { lock.writeLock().unlock(); } } @Override public void registerListener(final HierarchicalRepositoryChangeListener listener) { lock.writeLock().lock(); try { listeners.add(listener); if (listenersEnabled) { listener.onChange(); } } finally { lock.writeLock().unlock(); } } @Override public void unRegisterListener(final HierarchicalRepositoryChangeListener listener) { lock.writeLock().lock(); try { listeners.remove(listener); } finally { lock.writeLock().unlock(); } } /** * set the default value to fallback to if none found * * @param defaultValue the value */ @Override public void setDefault(final T defaultValue) { clearCache(); defaultmatch = defaultValue; } @Override public void clear() { lock.writeLock().lock(); try { clearCache(); listeners.clear(); matches.clear(); } finally { lock.writeLock().unlock(); } } @Override public void swap(Set<Map.Entry<String, T>> entries) { lock.writeLock().lock(); try { clearCache(); immutables.clear(); matches.clear(); for (Map.Entry<String, T> entry : entries) { addMatch(entry.getKey(), entry.getValue(), true, false); } } finally { lock.writeLock().unlock(); } onChange(); } @Override public void clearListeners() { listeners.clear(); } @Override public void clearCache() { cache.clear(); } private void onChange() { lock.readLock().lock(); try { if (listenersEnabled) { for (HierarchicalRepositoryChangeListener listener : listeners) { try { listener.onChange(); } catch (Throwable e) { ActiveMQServerLogger.LOGGER.errorCallingRepoListener(e); } } } } finally { lock.readLock().unlock(); } } /** * return any possible matches * * @param match * @return */ private Map<String, Match<T>> getPossibleMatches(final String match) { HashMap<String, Match<T>> possibleMatches = new HashMap<>(); for (Entry<String, Match<T>> entry : matches.entrySet()) { Match<T> entryMatch = entry.getValue(); if (entryMatch.getPattern().matcher(match).matches()) { possibleMatches.put(entry.getKey(), entryMatch); } } return possibleMatches; } /** * Compares to matches to see which one is more specific. */ private static final class MatchComparator implements Comparator<String>, Serializable { private static final long serialVersionUID = -6182535107518999740L; @Override public int compare(final String o1, final String o2) { if (o1.contains(Match.WILDCARD) && !o2.contains(Match.WILDCARD)) { return +1; } else if (!o1.contains(Match.WILDCARD) && o2.contains(Match.WILDCARD)) { return -1; } else if (o1.contains(Match.WILDCARD) && o2.contains(Match.WILDCARD)) { return o2.length() - o1.length(); } else if (o1.contains(Match.WORD_WILDCARD) && !o2.contains(Match.WORD_WILDCARD)) { return +1; } else if (!o1.contains(Match.WORD_WILDCARD) && o2.contains(Match.WORD_WILDCARD)) { return -1; } else if (o1.contains(Match.WORD_WILDCARD) && o2.contains(Match.WORD_WILDCARD)) { String[] leftSplits = o1.split("\\."); String[] rightSplits = o2.split("\\."); for (int i = 0; i < leftSplits.length; i++) { String left = leftSplits[i]; if (left.equals(Match.WORD_WILDCARD)) { if (rightSplits.length < i || !rightSplits[i].equals(Match.WORD_WILDCARD)) { return -1; } else { return +1; } } } } return o1.length() - o2.length(); } } }