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