Optimize getFilterInstance()
[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(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             .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             this.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(FilterDTO.createFilterDTO(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 {
291                 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
292                     LOG.error("couldn't parse property \"{}\"; skipping", key);
293                 }
294             }
295         }
296         return extractedConfig;
297     }
298
299     /**
300      * Register for config changes.
301      *
302      * @param listener
303      *            A listener implementing
304      *            <code>CustomFilterAdapterListener</code>
305      */
306     @Override
307     public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
308         LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
309         if (this.listeners.add(listener)) {
310             LOG.debug("Updated listener set: {}", listeners);
311             this.updateListener(listener);
312         }
313     }
314
315     private abstract static class FilterDTO {
316
317         private final Map<String, String> initParams;
318
319         protected FilterDTO(final Map<String, String> initParams) {
320             this.initParams = requireNonNull(initParams);
321         }
322
323         abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
324
325         static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
326             return new NamedFilterDTO(clazzName, initParams);
327         }
328
329         static FilterDTO createFilterDTO(final Filter instance) {
330             return new InstanceFilterDTO(instance);
331         }
332
333         /**
334          * Attempts to extract a map of key/value pairs from a given file.
335          *
336          * @return map with the initialization parameters.
337          */
338         Map<String, String> getInitParams() {
339             return initParams;
340         }
341     }
342
343     /**
344      * Essentially a tuple of (filterClassName, propertiesFileName). Allows
345      * quicker passing and return of Filter information.
346      */
347     private static class NamedFilterDTO extends FilterDTO {
348         private final String clazzName;
349
350         NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
351             super(initParams);
352             this.clazzName = requireNonNull(clazzName);
353         }
354
355         @SuppressWarnings("unchecked")
356         @Override
357         Filter getInstance(final Optional<ServletContext> servletContext) {
358             try {
359                 final Class<Filter> filterClazz = (Class<Filter>) Class.forName(clazzName);
360                 return init(filterClazz.getDeclaredConstructor().newInstance(), servletContext);
361             } catch (ReflectiveOperationException | ClassCastException e) {
362                 LOG.error("Error loading  {}", this, e);
363             }
364
365             return null;
366         }
367
368         private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
369             try {
370                 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
371                         getInitParams());
372                 filter.init(filterConfig);
373             } catch (ServletException e) {
374                 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
375             }
376
377             return filter;
378         }
379
380         @Override
381         public int hashCode() {
382             return clazzName.hashCode();
383         }
384
385         @Override
386         public boolean equals(final Object obj) {
387             if (this == obj) {
388                 return true;
389             }
390
391             if (obj == null) {
392                 return false;
393             }
394
395             if (getClass() != obj.getClass()) {
396                 return false;
397             }
398
399             NamedFilterDTO other = (NamedFilterDTO) obj;
400             return clazzName.equals(other.clazzName);
401         }
402
403         @Override
404         public String toString() {
405             return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
406         }
407     }
408
409     private static class InstanceFilterDTO extends FilterDTO {
410         private final Filter instance;
411
412         InstanceFilterDTO(final Filter instance) {
413             super(Collections.emptyMap());
414             this.instance = requireNonNull(instance);
415         }
416
417         @Override
418         Filter getInstance(final Optional<ServletContext> servletContext) {
419             return instance;
420         }
421
422         @Override
423         public int hashCode() {
424             return instance.hashCode();
425         }
426
427         @Override
428         public boolean equals(final Object obj) {
429             if (this == obj) {
430                 return true;
431             }
432
433             if (obj == null) {
434                 return false;
435             }
436
437             if (getClass() != obj.getClass()) {
438                 return false;
439             }
440
441             InstanceFilterDTO other = (InstanceFilterDTO) obj;
442             return instance.equals(other.instance);
443         }
444
445         @Override
446         public String toString() {
447             return "InstanceFilterDTO [instance=" + instance + "]";
448         }
449     }
450 }