806c732d308b9a1cb3ad17cd8b3b88f5ff64b4b4
[aaa.git] / aaa-filterchain / src / main / java / org / opendaylight / aaa / filterchain / configuration / impl / CustomFilterAdapterConfigurationImpl.java
1 /*
2  * Copyright (c) 2016, 2017 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.aaa.filterchain.configuration.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.base.Strings;
13 import com.google.common.collect.ImmutableList;
14 import com.google.common.collect.Iterators;
15 import com.google.common.collect.Streams;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.Enumeration;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.Objects;
23 import java.util.Optional;
24 import java.util.concurrent.ConcurrentHashMap;
25 import javax.servlet.Filter;
26 import javax.servlet.FilterConfig;
27 import javax.servlet.ServletContext;
28 import javax.servlet.ServletException;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterConfiguration;
31 import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterListener;
32 import org.osgi.service.component.annotations.Activate;
33 import org.osgi.service.component.annotations.Component;
34 import org.osgi.service.component.annotations.Modified;
35 import org.osgi.service.component.annotations.Reference;
36 import org.osgi.service.component.annotations.ReferenceCardinality;
37 import org.osgi.service.component.annotations.ReferencePolicy;
38 import org.osgi.service.component.annotations.ReferencePolicyOption;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * Implementation of CustomFilterAdapterConfiguration.
44  */
45 @Component(immediate = true, configurationPid = "org.opendaylight.aaa.filterchain")
46 public final class CustomFilterAdapterConfigurationImpl implements CustomFilterAdapterConfiguration {
47     private static final Logger LOG = LoggerFactory.getLogger(CustomFilterAdapterConfigurationImpl.class);
48
49     /**
50      * Separates different filter definitions. For example:
51      * <code>customFilterList = c.b.a.TestFilter1,f.d.e.TestFilter2,j.h.i.FilterN</code>
52      */
53     private static final String FILTER_DTO_SEPARATOR = ",";
54
55     /**
56      * <code>customFilterList</code> is the property advertised in the Karaf
57      * configuration admin.
58      */
59     static final String CUSTOM_FILTER_LIST_KEY = "customFilterList";
60
61     /**
62      * List of listeners to notify upon config admin events.
63      */
64     private final Collection<CustomFilterAdapterListener> listeners = ConcurrentHashMap.newKeySet();
65
66     /**
67      * Saves a local copy of the most recent configuration so when a listener is
68      * added, it can receive and initial update.
69      */
70     private volatile ImmutableList<FilterDTO> namedFilterDTOs = ImmutableList.of();
71
72     private volatile ImmutableList<FilterDTO> instanceFilterDTOs = ImmutableList.of();
73
74     @Activate
75     void activate(final Map<String, String> properties) {
76         update(properties);
77     }
78
79     @Modified
80     // Invoked in response to configuration admin changes
81     public void update(final Map<String, String> properties) {
82         if (properties != null) {
83             LOG.info("Custom filter properties updated: {}", properties);
84
85             namedFilterDTOs = getCustomFilterList(properties);
86             updateListeners();
87         }
88     }
89
90     // Invoked when a Filter OSGi service is added
91     @Reference(cardinality = ReferenceCardinality.MULTIPLE,
92             policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY,
93             // Needed to exclude any filters that is published for HTTP Whiteboard
94             // FIXME: it would be much better if we had a whitelist property to prevent confusion
95             target = "(!(|"
96                 + "(osgi.http.whiteboard.filter.pattern=*)"
97                 + "(osgi.http.whiteboard.filter.regex=*)"
98                 + "(osgi.http.whiteboard.filter.servlet=*)"
99                 + "))")
100     public void addFilter(final Filter filter) {
101         if (filter == null) {
102             return;
103         }
104
105         LOG.info("Custom Filter {} added", filter);
106         instanceFilterDTOs = ImmutableList.<FilterDTO>builder()
107             .addAll(instanceFilterDTOs)
108             .add(new InstanceFilterDTO(filter))
109             .build();
110         updateListeners();
111     }
112
113     // Invoked when a Filter OSGi service is removed
114     public void removeFilter(final Filter filter) {
115         if (filter == null) {
116             return;
117         }
118
119         LOG.info("Custom Filter {} removed", filter);
120         var toRemove = new InstanceFilterDTO(filter);
121         instanceFilterDTOs = instanceFilterDTOs.stream()
122             .filter(dto -> !dto.equals(toRemove))
123             .collect(ImmutableList.toImmutableList());
124         updateListeners();
125     }
126
127     /**
128      * Notify all listeners of a change event.
129      */
130     private void updateListeners() {
131         for (CustomFilterAdapterListener listener : listeners) {
132             updateListener(listener);
133         }
134     }
135
136     /**
137      * Update a particular listener with the new injected <code>FilterDTO</code>
138      * list.
139      *
140      * @param listener
141      *            The <code>CustomFilterAdapter</code> instance
142      * @param customFilterList
143      *            The newly injected <code>FilterDTO</code> list
144      */
145     private void updateListener(final CustomFilterAdapterListener listener) {
146         final var filterList = convertCustomFilterList(extractServletContext(listener));
147         LOG.debug("Notifying listener {} of filters {}", listener, filterList);
148         listener.updateInjectedFilters(filterList);
149     }
150
151     /**
152      * Utility method to extract a <code>ServletContext</code> from a listener's
153      * <code>FilterConfig</code>.
154      *
155      * @param listener
156      *            An object which listens for filter chain configuration
157      *            changes.
158      * @return An extracted <code>ServletContext</code>, or null if either the
159      *         <code>FilterConfig</code> of <code>ServletContext</code> is null
160      */
161     private static Optional<ServletContext> extractServletContext(final CustomFilterAdapterListener listener) {
162         final FilterConfig listenerFilterConfig = listener.getFilterConfig();
163         return listenerFilterConfig != null ? Optional.ofNullable(listenerFilterConfig.getServletContext())
164                 : Optional.empty();
165     }
166
167     /**
168      * Converts a List of class names (possibly Filters) and attempts to spawn
169      * corresponding <code>javax.servlet.Filter</code> instances.
170      *
171      * @param customFilterList
172      *            a list of class names, ideally Filters
173      * @return a list of derived Filter(s)
174      */
175     private ImmutableList<Filter> convertCustomFilterList(final Optional<ServletContext> listenerServletContext) {
176         return Streams.concat(namedFilterDTOs.stream(), instanceFilterDTOs.stream())
177             .map(filter -> getFilterInstance(filter, listenerServletContext))
178             .filter(Objects::nonNull)
179             .collect(ImmutableList.toImmutableList());
180     }
181
182     /**
183      * Utility method used to create and initialize a Filter from a FilterDTO.
184      *
185      * @param customFilter DTO containing Filter and properties path, if one exists.
186      * @param servletContext Scoped to the listener
187      * @return The Filter, or null
188      */
189     private static @Nullable Filter getFilterInstance(final FilterDTO customFilter,
190             final Optional<ServletContext> servletContext) {
191         final var filter = customFilter.getInstance(servletContext);
192         if (filter != null) {
193             LOG.info("Successfully loaded custom Filter {} for context {}", filter, servletContext);
194         }
195         return filter;
196     }
197
198     /**
199      * Allows creation of <code>FilterConfig</code> from a key/value properties file.
200      */
201     private static final class InjectedFilterConfig implements FilterConfig {
202
203         private final String filterName;
204         private final ServletContext servletContext;
205         private final Map<String, String> filterConfig;
206
207         // private for Factory Method pattern
208         private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
209                 final Map<String, String> filterConfig) {
210
211             filterName = filter.getClass().getSimpleName();
212             this.servletContext = servletContext.orElse(null);
213             this.filterConfig = filterConfig;
214         }
215
216         static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
217                 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
218             return new InjectedFilterConfig(filter, servletContext, filterConfig);
219         }
220
221         @Override
222         public String getFilterName() {
223             return filterName;
224         }
225
226         @Override
227         public String getInitParameter(final String paramName) {
228             return filterConfig != null ? filterConfig.get(paramName) : null;
229         }
230
231         @Override
232         public Enumeration<String> getInitParameterNames() {
233             return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
234                 : Collections.emptyEnumeration();
235         }
236
237         @Override
238         public ServletContext getServletContext() {
239             return servletContext;
240         }
241     }
242
243     /**
244      * Extracts the custom filter list as provided by Karaf Configuration Admin.
245      *
246      * @return A <code>non-null</code> <code>List</code> of the custom filter
247      *         fully qualified class names.
248      */
249     private static ImmutableList<FilterDTO> getCustomFilterList(final Map<String, String> configuration) {
250         final var customFilterListValue = configuration.get(CUSTOM_FILTER_LIST_KEY);
251         if (customFilterListValue == null) {
252             return ImmutableList.of();
253         }
254
255         final var builder = ImmutableList.<FilterDTO>builder();
256         // Creates the list from comma separate values; whitespace is removed first
257         for (var filterClazzName : customFilterListValue.replaceAll("\\s", "").split(FILTER_DTO_SEPARATOR)) {
258             if (!Strings.isNullOrEmpty(filterClazzName)) {
259                 builder.add(new NamedFilterDTO(filterClazzName,
260                     extractPropertiesForFilter(filterClazzName, configuration)));
261             }
262         }
263         return builder.build();
264     }
265
266     /**
267      * Extract a subset of properties that apply to a particular Filter.
268      *
269      * @param clazzName
270      *            prefix used to specify key value pair (i.e.,
271      *            a.b.c.Filter.property)
272      * @param fullConfiguration
273      *            The entire configuration dictionary, which is traversed for
274      *            applicable properties.
275      * @return A Map of applicable properties for a filter.
276      */
277     private static Map<String, String> extractPropertiesForFilter(final String clazzName,
278             final Map<String, String> fullConfiguration) {
279
280         final Map<String, String> extractedConfig = new HashMap<>();
281         for (Entry<String, String> entry : fullConfiguration.entrySet()) {
282             String key = entry.getKey();
283             final int lastDotSeparator = key.lastIndexOf(".");
284             if (lastDotSeparator >= 0) {
285                 final String comparisonClazzNameSubstring = key.substring(0, lastDotSeparator);
286                 if (comparisonClazzNameSubstring.equals(clazzName)) {
287                     final String filterInitParamKey = key.substring(lastDotSeparator + 1);
288                     extractedConfig.put(filterInitParamKey, entry.getValue());
289                 }
290             } else if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
291                 LOG.error("couldn't parse property \"{}\"; skipping", key);
292             }
293         }
294         return extractedConfig;
295     }
296
297     /**
298      * Register for config changes.
299      *
300      * @param listener
301      *            A listener implementing
302      *            <code>CustomFilterAdapterListener</code>
303      */
304     @Override
305     public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
306         LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
307         if (listeners.add(listener)) {
308             LOG.debug("Updated listener set: {}", listeners);
309             updateListener(listener);
310         }
311     }
312
313     private abstract static sealed class FilterDTO {
314
315         abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
316
317         @Override
318         public abstract int hashCode();
319
320         @Override
321         public abstract boolean equals(Object obj);
322
323         @Override
324         public abstract String toString();
325     }
326
327     /**
328      * Essentially a tuple of (filterClassName, propertiesFileName). Allows
329      * quicker passing and return of Filter information.
330      */
331     private static final class NamedFilterDTO extends FilterDTO {
332         private final Map<String, String> initParams;
333         private final String clazzName;
334
335         NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
336             this.clazzName = requireNonNull(clazzName);
337             this.initParams = requireNonNull(initParams);
338         }
339
340         @Override
341         Filter getInstance(final Optional<ServletContext> servletContext) {
342             final Filter instance;
343             try {
344                 instance = Class.forName(clazzName).asSubclass(Filter.class).getDeclaredConstructor().newInstance();
345             } catch (ReflectiveOperationException | ClassCastException e) {
346                 LOG.error("Error loading {}", this, e);
347                 return null;
348             }
349             return init(instance, servletContext);
350         }
351
352         private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
353             try {
354                 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
355                     initParams);
356                 filter.init(filterConfig);
357             } catch (ServletException e) {
358                 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
359             }
360
361             return filter;
362         }
363
364         @Override
365         public int hashCode() {
366             return clazzName.hashCode();
367         }
368
369         @Override
370         public boolean equals(final Object obj) {
371             return this == obj || obj instanceof NamedFilterDTO other && clazzName.equals(other.clazzName);
372         }
373
374         @Override
375         public String toString() {
376             return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + initParams + "]";
377         }
378     }
379
380     private static final class InstanceFilterDTO extends FilterDTO {
381         private final Filter instance;
382
383         InstanceFilterDTO(final Filter instance) {
384             this.instance = requireNonNull(instance);
385         }
386
387         @Override
388         Filter getInstance(final Optional<ServletContext> servletContext) {
389             return instance;
390         }
391
392         @Override
393         public int hashCode() {
394             return instance.hashCode();
395         }
396
397         @Override
398         public boolean equals(final Object obj) {
399             return this == obj || obj instanceof InstanceFilterDTO other && instance.equals(other.instance);
400         }
401
402         @Override
403         public String toString() {
404             return "InstanceFilterDTO [instance=" + instance + "]";
405         }
406     }
407 }