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.filter;
018
019import java.util.Comparator;
020import java.util.HashSet;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Set;
024import java.util.SortedSet;
025import java.util.TreeSet;
026import java.util.function.Supplier;
027import java.util.stream.Collectors;
028
029import org.apache.activemq.command.ActiveMQDestination;
030
031/**
032 * A Map-like data structure allowing values to be indexed by
033 * {@link ActiveMQDestination} and retrieved by destination - supporting both *
034 * and &gt; style of wildcard as well as composite destinations. <br>
035 * This class assumes that the index changes rarely but that fast lookup into
036 * the index is required. So this class maintains a pre-calculated index for
037 * destination steps. So looking up the values for "TEST.*" or "*.TEST" will be
038 * pretty fast. <br>
039 * Looking up of a value could return a single value or a List of matching
040 * values if a wildcard or composite destination is used.
041 */
042public class DestinationMap {
043    protected static final String ANY_DESCENDENT = DestinationFilter.ANY_DESCENDENT;
044    protected static final String ANY_CHILD = DestinationFilter.ANY_CHILD;
045
046    private DestinationMapNode queueRootNode = new DestinationMapNode(null);
047    private DestinationMapNode tempQueueRootNode = new DestinationMapNode(null);
048    private DestinationMapNode topicRootNode = new DestinationMapNode(null);
049    private DestinationMapNode tempTopicRootNode = new DestinationMapNode(null);
050
051
052    /**
053     * Looks up the value(s) matching the given Destination key. For simple
054     * destinations this is typically a List of one single value, for wildcards
055     * or composite destinations this will typically be a List of matching
056     * values.
057     *
058     * @param key the destination to lookup
059     * @return a List of matching values or an empty list if there are no
060     *         matching values.
061     */
062    @SuppressWarnings({"rawtypes", "unchecked"})
063    public synchronized Set get(ActiveMQDestination key) {
064        if (key.isComposite()) {
065            ActiveMQDestination[] destinations = key.getCompositeDestinations();
066            Set answer = new HashSet(destinations.length);
067            for (int i = 0; i < destinations.length; i++) {
068                ActiveMQDestination childDestination = destinations[i];
069                Object value = get(childDestination);
070                if (value instanceof Set) {
071                    answer.addAll((Set) value);
072                } else if (value != null) {
073                    answer.add(value);
074                }
075            }
076            return answer;
077        }
078        return findWildcardMatches(key);
079    }
080
081    public synchronized void put(ActiveMQDestination key, Object value) {
082        if (key.isComposite()) {
083            ActiveMQDestination[] destinations = key.getCompositeDestinations();
084            for (int i = 0; i < destinations.length; i++) {
085                ActiveMQDestination childDestination = destinations[i];
086                put(childDestination, value);
087            }
088            return;
089        }
090        String[] paths = key.getDestinationPaths();
091        getRootNode(key).add(paths, 0, value);
092    }
093
094
095    /**
096     * Removes the value from the associated destination
097     */
098    public synchronized void remove(ActiveMQDestination key, Object value) {
099        if (key.isComposite()) {
100            ActiveMQDestination[] destinations = key.getCompositeDestinations();
101            for (int i = 0; i < destinations.length; i++) {
102                ActiveMQDestination childDestination = destinations[i];
103                remove(childDestination, value);
104            }
105            return;
106        }
107        String[] paths = key.getDestinationPaths();
108        getRootNode(key).remove(paths, 0, value);
109
110    }
111
112    public int getTopicRootChildCount() {
113        return topicRootNode.getChildCount();
114    }
115
116    public int getQueueRootChildCount() {
117        return queueRootNode.getChildCount();
118    }
119
120    public DestinationMapNode getQueueRootNode() {
121        return queueRootNode;
122    }
123
124    public DestinationMapNode getTopicRootNode() {
125        return topicRootNode;
126    }
127
128    public DestinationMapNode getTempQueueRootNode() {
129        return tempQueueRootNode;
130    }
131
132    public DestinationMapNode getTempTopicRootNode() {
133        return tempTopicRootNode;
134    }
135
136    // Implementation methods
137    // -------------------------------------------------------------------------
138
139    /**
140     * A helper method to allow the destination map to be populated from a
141     * dependency injection framework such as Spring
142     */
143    @SuppressWarnings({"rawtypes"})
144    protected void setEntries(List<DestinationMapEntry> entries) {
145        for (Object element : entries) {
146            Class<? extends DestinationMapEntry> type = getEntryClass();
147            if (type.isInstance(element)) {
148                DestinationMapEntry entry = (DestinationMapEntry) element;
149                put(entry.getDestination(), entry.getValue());
150            } else {
151                throw new IllegalArgumentException("Each entry must be an instance of type: " + type.getName() + " but was: " + element);
152            }
153        }
154    }
155
156    /**
157     * Returns the type of the allowed entries which can be set via the
158     * {@link #setEntries(List)} method. This allows derived classes to further
159     * restrict the type of allowed entries to make a type safe destination map
160     * for custom policies.
161     */
162    @SuppressWarnings({"rawtypes"})
163    protected Class<? extends DestinationMapEntry> getEntryClass() {
164        return DestinationMapEntry.class;
165    }
166
167    @SuppressWarnings({"rawtypes", "unchecked"})
168    protected Set findWildcardMatches(ActiveMQDestination key) {
169       return findWildcardMatches(key, true);
170    }
171
172    @SuppressWarnings({"rawtypes", "unchecked"})
173    protected Set findWildcardMatches(ActiveMQDestination key, boolean deep) {
174        String[] paths = key.getDestinationPaths();
175        Set answer = new HashSet();
176        getRootNode(key).appendMatchingValues(answer, paths, 0, deep);
177        return answer;
178    }
179
180    /**
181     * @param key
182     * @return
183     */
184    @SuppressWarnings({"rawtypes", "unchecked"})
185    public Set removeAll(ActiveMQDestination key) {
186        Set rc = new HashSet();
187        if (key.isComposite()) {
188            ActiveMQDestination[] destinations = key.getCompositeDestinations();
189            for (int i = 0; i < destinations.length; i++) {
190                rc.add(removeAll(destinations[i]));
191            }
192            return rc;
193        }
194        String[] paths = key.getDestinationPaths();
195        getRootNode(key).removeAll(rc, paths, 0);
196        return rc;
197    }
198
199    /**
200     * Returns the value which matches the given destination or null if there is
201     * no matching value. If there are multiple values, the results are sorted
202     * and the last item (the biggest) is returned.
203     *
204     * @param destination the destination to find the value for
205     * @return the largest matching value or null if no value matches
206     */
207    @SuppressWarnings({"rawtypes", "unchecked"})
208    public DestinationMapEntry chooseValue(final ActiveMQDestination destination) {
209        Set<DestinationMapEntry> set = get(destination);
210        if (set == null || set.isEmpty()) {
211            return null;
212        }
213
214        //Comparator to sort in order - we want to pick the exact match by destination or the
215        //closest parent that applies
216        final Comparator<DestinationMapEntry> comparator = new Comparator<DestinationMapEntry>() {
217            @Override
218            public int compare(DestinationMapEntry entry1, DestinationMapEntry entry2) {
219                return destination.equals(entry1.destination) ? -1 : (destination.equals(entry2.destination) ? 1 : entry1.compareTo(entry2));
220            }
221        };
222
223        //Sort and filter out any children and non matching entries
224        final SortedSet<DestinationMapEntry> sortedSet = set.stream()
225            .filter(entry -> isMatchOrParent(destination, (DestinationMapEntry)entry))
226            .collect(Collectors.toCollection(() -> new TreeSet<DestinationMapEntry>(comparator)));
227
228        return sortedSet.size() > 0 ? sortedSet.first() : null;
229    }
230
231    @SuppressWarnings("rawtypes")
232    //Used to filter out any child/unmatching entries
233    private boolean isMatchOrParent(final ActiveMQDestination destination, final DestinationMapEntry entry) {
234        //If destination not set then do not filter out
235        if (entry.getDestination() == null) {
236            return true;
237        }
238        final DestinationFilter filter = DestinationFilter.parseFilter(entry.getDestination());
239        return destination.equals(entry.getDestination()) || filter.matches(destination);
240    }
241
242    /**
243     * Returns the root node for the given destination type
244     */
245    protected DestinationMapNode getRootNode(ActiveMQDestination key) {
246        if (key.isTemporary()) {
247            if (key.isQueue()) {
248                return tempQueueRootNode;
249            } else {
250                return tempTopicRootNode;
251            }
252        } else {
253            if (key.isQueue()) {
254                return queueRootNode;
255            } else {
256                return topicRootNode;
257            }
258        }
259    }
260
261    public void reset() {
262        queueRootNode = new DestinationMapNode(null);
263        tempQueueRootNode = new DestinationMapNode(null);
264        topicRootNode = new DestinationMapNode(null);
265        tempTopicRootNode = new DestinationMapNode(null);
266    }
267
268    public boolean isEmpty() {
269        return queueRootNode.isEmpty() && topicRootNode.isEmpty() && tempQueueRootNode.isEmpty() && tempTopicRootNode.isEmpty();
270    }
271
272    public static Set union(Set existing, Set candidates) {
273        if (candidates != null) {
274            if (existing != null) {
275                for (Iterator<Object> iterator = existing.iterator(); iterator.hasNext(); ) {
276                    Object toMatch = iterator.next();
277                    if (!candidates.contains(toMatch)) {
278                        iterator.remove();
279                    }
280                }
281            } else {
282                existing = candidates;
283            }
284        } else if (existing != null) {
285            existing.clear();
286        }
287        return existing;
288    }
289
290}