Iterate over simple array
[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 List<FilterDTO> namedFilterDTOs = Collections.emptyList();
72
73     private volatile List<FilterDTO> instanceFilterDTOs = Collections.emptyList();
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             this.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         this.instanceFilterDTOs = ImmutableList.<FilterDTO>builder().addAll(instanceFilterDTOs)
108                 .add(FilterDTO.createFilterDTO(filter)).build();
109         updateListeners();
110     }
111
112     // Invoked when a Filter OSGi service is removed
113     public void removeFilter(final Filter filter) {
114         if (filter == null) {
115             return;
116         }
117
118         LOG.info("Custom Filter {} removed", filter);
119         FilterDTO toRemove = FilterDTO.createFilterDTO(filter);
120         this.instanceFilterDTOs = ImmutableList.copyOf(instanceFilterDTOs.stream().filter(dto -> !dto.equals(toRemove))
121                 .collect(Collectors.toList()));
122         updateListeners();
123     }
124
125     /**
126      * Notify all listeners of a change event.
127      */
128     private void updateListeners() {
129         for (CustomFilterAdapterListener listener : listeners) {
130             updateListener(listener);
131         }
132     }
133
134     /**
135      * Update a particular listener with the new injected <code>FilterDTO</code>
136      * list.
137      *
138      * @param listener
139      *            The <code>CustomFilterAdapter</code> instance
140      * @param customFilterList
141      *            The newly injected <code>FilterDTO</code> list
142      */
143     private void updateListener(final CustomFilterAdapterListener listener) {
144         final Optional<ServletContext> listenerServletContext = extractServletContext(listener);
145         final List<Filter> filterList = convertCustomFilterList(listenerServletContext);
146
147         LOG.debug("Notifying listener {} of filters {}", listener, filterList);
148
149         listener.updateInjectedFilters(filterList);
150     }
151
152     /**
153      * Utility method to extract a <code>ServletContext</code> from a listener's
154      * <code>FilterConfig</code>.
155      *
156      * @param listener
157      *            An object which listens for filter chain configuration
158      *            changes.
159      * @return An extracted <code>ServletContext</code>, or null if either the
160      *         <code>FilterConfig</code> of <code>ServletContext</code> is null
161      */
162     private static Optional<ServletContext> extractServletContext(final CustomFilterAdapterListener listener) {
163         final FilterConfig listenerFilterConfig = listener.getFilterConfig();
164         return listenerFilterConfig != null ? Optional.ofNullable(listenerFilterConfig.getServletContext())
165                 : Optional.empty();
166     }
167
168     /**
169      * Converts a List of class names (possibly Filters) and attempts to spawn
170      * corresponding <code>javax.servlet.Filter</code> instances.
171      *
172      * @param customFilterList
173      *            a list of class names, ideally Filters
174      * @return a list of derived Filter(s)
175      */
176     private List<Filter> convertCustomFilterList(final Optional<ServletContext> listenerServletContext) {
177         final List<Filter> filterList = ImmutableList.<FilterDTO>builder().addAll(namedFilterDTOs)
178             .addAll(instanceFilterDTOs).build().stream().flatMap(
179                 filter -> getFilterInstance(filter, listenerServletContext)).collect(Collectors.toList());
180         return Collections.unmodifiableList(filterList);
181     }
182
183     /**
184      * Utility method used to create and initialize a Filter from a FilterDTO.
185      *
186      * @param customFilter
187      *            DTO containing Filter and properties path, if one exists.
188      * @param servletContext
189      *            Scoped to the listener
190      * @return A Stream containing the Filter, or empty if one cannot be instantiated.
191      */
192     private static Stream<Filter> getFilterInstance(final FilterDTO customFilter,
193             final Optional<ServletContext> servletContext) {
194         final Filter filter = customFilter.getInstance(servletContext);
195         if (filter != null) {
196             LOG.info("Successfully loaded custom Filter {} for context {}", filter, servletContext);
197             return Stream.of(filter);
198         }
199
200         return Stream.empty();
201     }
202
203     /**
204      * Allows creation of <code>FilterConfig</code> from a key/value properties file.
205      */
206     private static final class InjectedFilterConfig implements FilterConfig {
207
208         private final String filterName;
209         private final ServletContext servletContext;
210         private final Map<String, String> filterConfig;
211
212         // private for Factory Method pattern
213         private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
214                 final Map<String, String> filterConfig) {
215
216             this.filterName = filter.getClass().getSimpleName();
217             this.servletContext = servletContext.orElse(null);
218             this.filterConfig = filterConfig;
219         }
220
221         static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
222                 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
223             return new InjectedFilterConfig(filter, servletContext, filterConfig);
224         }
225
226         @Override
227         public String getFilterName() {
228             return filterName;
229         }
230
231         @Override
232         public String getInitParameter(final String paramName) {
233             return filterConfig != null ? filterConfig.get(paramName) : null;
234         }
235
236         @Override
237         public Enumeration<String> getInitParameterNames() {
238             return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
239                 : Collections.emptyEnumeration();
240         }
241
242         @Override
243         public ServletContext getServletContext() {
244             return servletContext;
245         }
246     }
247
248     /**
249      * Extracts the custom filter list as provided by Karaf Configuration Admin.
250      *
251      * @return A <code>non-null</code> <code>List</code> of the custom filter
252      *         fully qualified class names.
253      */
254     private static List<FilterDTO> getCustomFilterList(final Map<String, String> configuration) {
255         final String customFilterListValue = configuration.get(CUSTOM_FILTER_LIST_KEY);
256         final ImmutableList.Builder<FilterDTO> customFilterListBuilder = ImmutableList.builder();
257         if (customFilterListValue != null) {
258             // Creates the list from comma separate values; whitespace is removed first
259             for (String filterClazzName : customFilterListValue.replaceAll("\\s", "").split(FILTER_DTO_SEPARATOR)) {
260                 if (!Strings.isNullOrEmpty(filterClazzName)) {
261                     final Map<String, String> applicableConfigs = extractPropertiesForFilter(filterClazzName,
262                             configuration);
263                     final FilterDTO filterDTO = FilterDTO.createFilterDTO(filterClazzName, applicableConfigs);
264                     customFilterListBuilder.add(filterDTO);
265                 }
266             }
267         }
268         return customFilterListBuilder.build();
269     }
270
271     /**
272      * Extract a subset of properties that apply to a particular Filter.
273      *
274      * @param clazzName
275      *            prefix used to specify key value pair (i.e.,
276      *            a.b.c.Filter.property)
277      * @param fullConfiguration
278      *            The entire configuration dictionary, which is traversed for
279      *            applicable properties.
280      * @return A Map of applicable properties for a filter.
281      */
282     private static Map<String, String> extractPropertiesForFilter(final String clazzName,
283             final Map<String, String> fullConfiguration) {
284
285         final Map<String, String> extractedConfig = new HashMap<>();
286         for (Entry<String, String> entry : fullConfiguration.entrySet()) {
287             String key = entry.getKey();
288             final int lastDotSeparator = key.lastIndexOf(".");
289             if (lastDotSeparator >= 0) {
290                 final String comparisonClazzNameSubstring = key.substring(0, lastDotSeparator);
291                 if (comparisonClazzNameSubstring.equals(clazzName)) {
292                     final String filterInitParamKey = key.substring(lastDotSeparator + 1);
293                     extractedConfig.put(filterInitParamKey, entry.getValue());
294                 }
295             } else {
296                 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
297                     LOG.error("couldn't parse property \"{}\"; skipping", key);
298                 }
299             }
300         }
301         return extractedConfig;
302     }
303
304     /**
305      * Register for config changes.
306      *
307      * @param listener
308      *            A listener implementing
309      *            <code>CustomFilterAdapterListener</code>
310      */
311     @Override
312     public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
313         LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
314         if (this.listeners.add(listener)) {
315             LOG.debug("Updated listener set: {}", listeners);
316             this.updateListener(listener);
317         }
318     }
319
320     private abstract static class FilterDTO {
321
322         private final Map<String, String> initParams;
323
324         protected FilterDTO(final Map<String, String> initParams) {
325             this.initParams = requireNonNull(initParams);
326         }
327
328         abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
329
330         static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
331             return new NamedFilterDTO(clazzName, initParams);
332         }
333
334         static FilterDTO createFilterDTO(final Filter instance) {
335             return new InstanceFilterDTO(instance);
336         }
337
338         /**
339          * Attempts to extract a map of key/value pairs from a given file.
340          *
341          * @return map with the initialization parameters.
342          */
343         Map<String, String> getInitParams() {
344             return initParams;
345         }
346     }
347
348     /**
349      * Essentially a tuple of (filterClassName, propertiesFileName). Allows
350      * quicker passing and return of Filter information.
351      */
352     private static class NamedFilterDTO extends FilterDTO {
353         private final String clazzName;
354
355         NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
356             super(initParams);
357             this.clazzName = requireNonNull(clazzName);
358         }
359
360         @SuppressWarnings("unchecked")
361         @Override
362         Filter getInstance(final Optional<ServletContext> servletContext) {
363             try {
364                 final Class<Filter> filterClazz = (Class<Filter>) Class.forName(clazzName);
365                 return init(filterClazz.getDeclaredConstructor().newInstance(), servletContext);
366             } catch (ReflectiveOperationException | ClassCastException e) {
367                 LOG.error("Error loading  {}", this, e);
368             }
369
370             return null;
371         }
372
373         private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
374             try {
375                 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
376                         getInitParams());
377                 filter.init(filterConfig);
378             } catch (ServletException e) {
379                 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
380             }
381
382             return filter;
383         }
384
385         @Override
386         public int hashCode() {
387             return clazzName.hashCode();
388         }
389
390         @Override
391         public boolean equals(final Object obj) {
392             if (this == obj) {
393                 return true;
394             }
395
396             if (obj == null) {
397                 return false;
398             }
399
400             if (getClass() != obj.getClass()) {
401                 return false;
402             }
403
404             NamedFilterDTO other = (NamedFilterDTO) obj;
405             return clazzName.equals(other.clazzName);
406         }
407
408         @Override
409         public String toString() {
410             return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
411         }
412     }
413
414     private static class InstanceFilterDTO extends FilterDTO {
415         private final Filter instance;
416
417         InstanceFilterDTO(final Filter instance) {
418             super(Collections.emptyMap());
419             this.instance = requireNonNull(instance);
420         }
421
422         @Override
423         Filter getInstance(final Optional<ServletContext> servletContext) {
424             return instance;
425         }
426
427         @Override
428         public int hashCode() {
429             return instance.hashCode();
430         }
431
432         @Override
433         public boolean equals(final Object obj) {
434             if (this == obj) {
435                 return true;
436             }
437
438             if (obj == null) {
439                 return false;
440             }
441
442             if (getClass() != obj.getClass()) {
443                 return false;
444             }
445
446             InstanceFilterDTO other = (InstanceFilterDTO) obj;
447             return instance.equals(other.instance);
448         }
449
450         @Override
451         public String toString() {
452             return "InstanceFilterDTO [instance=" + instance + "]";
453         }
454     }
455 }