/*
* 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();
}
}
}