Optimize convertCustomFilterList()
[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.Optional;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.stream.Stream;
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(FilterDTO.createFilterDTO(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         FilterDTO toRemove = FilterDTO.createFilterDTO(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             .flatMap(filter -> getFilterInstance(filter, listenerServletContext))
178             .collect(ImmutableList.toImmutableList());
179     }
180
181     /**
182      * Utility method used to create and initialize a Filter from a FilterDTO.
183      *
184      * @param customFilter
185      *            DTO containing Filter and properties path, if one exists.
186      * @param servletContext
187      *            Scoped to the listener
188      * @return A Stream containing the Filter, or empty if one cannot be instantiated.
189      */
190     private static Stream<Filter> getFilterInstance(final FilterDTO customFilter,
191             final Optional<ServletContext> servletContext) {
192         final Filter filter = customFilter.getInstance(servletContext);
193         if (filter != null) {
194             LOG.info("Successfully loaded custom Filter {} for context {}", filter, servletContext);
195             return Stream.of(filter);
196         }
197
198         return Stream.empty();
199     }
200
201     /**
202      * Allows creation of <code>FilterConfig</code> from a key/value properties file.
203      */
204     private static final class InjectedFilterConfig implements FilterConfig {
205
206         private final String filterName;
207         private final ServletContext servletContext;
208         private final Map<String, String> filterConfig;
209
210         // private for Factory Method pattern
211         private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
212                 final Map<String, String> filterConfig) {
213
214             this.filterName = filter.getClass().getSimpleName();
215             this.servletContext = servletContext.orElse(null);
216             this.filterConfig = filterConfig;
217         }
218
219         static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
220                 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
221             return new InjectedFilterConfig(filter, servletContext, filterConfig);
222         }
223
224         @Override
225         public String getFilterName() {
226             return filterName;
227         }
228
229         @Override
230         public String getInitParameter(final String paramName) {
231             return filterConfig != null ? filterConfig.get(paramName) : null;
232         }
233
234         @Override
235         public Enumeration<String> getInitParameterNames() {
236             return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
237                 : Collections.emptyEnumeration();
238         }
239
240         @Override
241         public ServletContext getServletContext() {
242             return servletContext;
243         }
244     }
245
246     /**
247      * Extracts the custom filter list as provided by Karaf Configuration Admin.
248      *
249      * @return A <code>non-null</code> <code>List</code> of the custom filter
250      *         fully qualified class names.
251      */
252     private static ImmutableList<FilterDTO> getCustomFilterList(final Map<String, String> configuration) {
253         final var customFilterListValue = configuration.get(CUSTOM_FILTER_LIST_KEY);
254         if (customFilterListValue == null) {
255             return ImmutableList.of();
256         }
257
258         final var builder = ImmutableList.<FilterDTO>builder();
259         // Creates the list from comma separate values; whitespace is removed first
260         for (var filterClazzName : customFilterListValue.replaceAll("\\s", "").split(FILTER_DTO_SEPARATOR)) {
261             if (!Strings.isNullOrEmpty(filterClazzName)) {
262                 builder.add(FilterDTO.createFilterDTO(filterClazzName,
263                     extractPropertiesForFilter(filterClazzName, configuration)));
264             }
265         }
266         return builder.build();
267     }
268
269     /**
270      * Extract a subset of properties that apply to a particular Filter.
271      *
272      * @param clazzName
273      *            prefix used to specify key value pair (i.e.,
274      *            a.b.c.Filter.property)
275      * @param fullConfiguration
276      *            The entire configuration dictionary, which is traversed for
277      *            applicable properties.
278      * @return A Map of applicable properties for a filter.
279      */
280     private static Map<String, String> extractPropertiesForFilter(final String clazzName,
281             final Map<String, String> fullConfiguration) {
282
283         final Map<String, String> extractedConfig = new HashMap<>();
284         for (Entry<String, String> entry : fullConfiguration.entrySet()) {
285             String key = entry.getKey();
286             final int lastDotSeparator = key.lastIndexOf(".");
287             if (lastDotSeparator >= 0) {
288                 final String comparisonClazzNameSubstring = key.substring(0, lastDotSeparator);
289                 if (comparisonClazzNameSubstring.equals(clazzName)) {
290                     final String filterInitParamKey = key.substring(lastDotSeparator + 1);
291                     extractedConfig.put(filterInitParamKey, entry.getValue());
292                 }
293             } else {
294                 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
295                     LOG.error("couldn't parse property \"{}\"; skipping", key);
296                 }
297             }
298         }
299         return extractedConfig;
300     }
301
302     /**
303      * Register for config changes.
304      *
305      * @param listener
306      *            A listener implementing
307      *            <code>CustomFilterAdapterListener</code>
308      */
309     @Override
310     public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
311         LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
312         if (this.listeners.add(listener)) {
313             LOG.debug("Updated listener set: {}", listeners);
314             this.updateListener(listener);
315         }
316     }
317
318     private abstract static class FilterDTO {
319
320         private final Map<String, String> initParams;
321
322         protected FilterDTO(final Map<String, String> initParams) {
323             this.initParams = requireNonNull(initParams);
324         }
325
326         abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
327
328         static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
329             return new NamedFilterDTO(clazzName, initParams);
330         }
331
332         static FilterDTO createFilterDTO(final Filter instance) {
333             return new InstanceFilterDTO(instance);
334         }
335
336         /**
337          * Attempts to extract a map of key/value pairs from a given file.
338          *
339          * @return map with the initialization parameters.
340          */
341         Map<String, String> getInitParams() {
342             return initParams;
343         }
344     }
345
346     /**
347      * Essentially a tuple of (filterClassName, propertiesFileName). Allows
348      * quicker passing and return of Filter information.
349      */
350     private static class NamedFilterDTO extends FilterDTO {
351         private final String clazzName;
352
353         NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
354             super(initParams);
355             this.clazzName = requireNonNull(clazzName);
356         }
357
358         @SuppressWarnings("unchecked")
359         @Override
360         Filter getInstance(final Optional<ServletContext> servletContext) {
361             try {
362                 final Class<Filter> filterClazz = (Class<Filter>) Class.forName(clazzName);
363                 return init(filterClazz.getDeclaredConstructor().newInstance(), servletContext);
364             } catch (ReflectiveOperationException | ClassCastException e) {
365                 LOG.error("Error loading  {}", this, e);
366             }
367
368             return null;
369         }
370
371         private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
372             try {
373                 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
374                         getInitParams());
375                 filter.init(filterConfig);
376             } catch (ServletException e) {
377                 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
378             }
379
380             return filter;
381         }
382
383         @Override
384         public int hashCode() {
385             return clazzName.hashCode();
386         }
387
388         @Override
389         public boolean equals(final Object obj) {
390             if (this == obj) {
391                 return true;
392             }
393
394             if (obj == null) {
395                 return false;
396             }
397
398             if (getClass() != obj.getClass()) {
399                 return false;
400             }
401
402             NamedFilterDTO other = (NamedFilterDTO) obj;
403             return clazzName.equals(other.clazzName);
404         }
405
406         @Override
407         public String toString() {
408             return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
409         }
410     }
411
412     private static class InstanceFilterDTO extends FilterDTO {
413         private final Filter instance;
414
415         InstanceFilterDTO(final Filter instance) {
416             super(Collections.emptyMap());
417             this.instance = requireNonNull(instance);
418         }
419
420         @Override
421         Filter getInstance(final Optional<ServletContext> servletContext) {
422             return instance;
423         }
424
425         @Override
426         public int hashCode() {
427             return instance.hashCode();
428         }
429
430         @Override
431         public boolean equals(final Object obj) {
432             if (this == obj) {
433                 return true;
434             }
435
436             if (obj == null) {
437                 return false;
438             }
439
440             if (getClass() != obj.getClass()) {
441                 return false;
442             }
443
444             InstanceFilterDTO other = (InstanceFilterDTO) obj;
445             return instance.equals(other.instance);
446         }
447
448         @Override
449         public String toString() {
450             return "InstanceFilterDTO [instance=" + instance + "]";
451         }
452     }
453 }