2 * Copyright (c) 2016, 2017 Brocade Communications Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.aaa.filterchain.configuration.impl;
10 import static java.util.Objects.requireNonNull;
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;
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;
43 * Implementation of CustomFilterAdapterConfiguration.
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);
50 * Separates different filter definitions. For example:
51 * <code>customFilterList = c.b.a.TestFilter1,f.d.e.TestFilter2,j.h.i.FilterN</code>
53 private static final String FILTER_DTO_SEPARATOR = ",";
56 * <code>customFilterList</code> is the property advertised in the Karaf
57 * configuration admin.
59 static final String CUSTOM_FILTER_LIST_KEY = "customFilterList";
62 * List of listeners to notify upon config admin events.
64 private final Collection<CustomFilterAdapterListener> listeners = ConcurrentHashMap.newKeySet();
67 * Saves a local copy of the most recent configuration so when a listener is
68 * added, it can receive and initial update.
70 private volatile ImmutableList<FilterDTO> namedFilterDTOs = ImmutableList.of();
72 private volatile ImmutableList<FilterDTO> instanceFilterDTOs = ImmutableList.of();
75 void activate(final Map<String, String> properties) {
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);
85 namedFilterDTOs = getCustomFilterList(properties);
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
96 + "(osgi.http.whiteboard.filter.pattern=*)"
97 + "(osgi.http.whiteboard.filter.regex=*)"
98 + "(osgi.http.whiteboard.filter.servlet=*)"
100 public void addFilter(final Filter filter) {
101 if (filter == null) {
105 LOG.info("Custom Filter {} added", filter);
106 instanceFilterDTOs = ImmutableList.<FilterDTO>builder()
107 .addAll(instanceFilterDTOs)
108 .add(FilterDTO.createFilterDTO(filter))
113 // Invoked when a Filter OSGi service is removed
114 public void removeFilter(final Filter filter) {
115 if (filter == null) {
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());
128 * Notify all listeners of a change event.
130 private void updateListeners() {
131 for (CustomFilterAdapterListener listener : listeners) {
132 updateListener(listener);
137 * Update a particular listener with the new injected <code>FilterDTO</code>
141 * The <code>CustomFilterAdapter</code> instance
142 * @param customFilterList
143 * The newly injected <code>FilterDTO</code> list
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);
152 * Utility method to extract a <code>ServletContext</code> from a listener's
153 * <code>FilterConfig</code>.
156 * An object which listens for filter chain configuration
158 * @return An extracted <code>ServletContext</code>, or null if either the
159 * <code>FilterConfig</code> of <code>ServletContext</code> is null
161 private static Optional<ServletContext> extractServletContext(final CustomFilterAdapterListener listener) {
162 final FilterConfig listenerFilterConfig = listener.getFilterConfig();
163 return listenerFilterConfig != null ? Optional.ofNullable(listenerFilterConfig.getServletContext())
168 * Converts a List of class names (possibly Filters) and attempts to spawn
169 * corresponding <code>javax.servlet.Filter</code> instances.
171 * @param customFilterList
172 * a list of class names, ideally Filters
173 * @return a list of derived Filter(s)
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());
182 * Utility method used to create and initialize a Filter from a FilterDTO.
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.
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);
198 return Stream.empty();
202 * Allows creation of <code>FilterConfig</code> from a key/value properties file.
204 private static final class InjectedFilterConfig implements FilterConfig {
206 private final String filterName;
207 private final ServletContext servletContext;
208 private final Map<String, String> filterConfig;
210 // private for Factory Method pattern
211 private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
212 final Map<String, String> filterConfig) {
214 this.filterName = filter.getClass().getSimpleName();
215 this.servletContext = servletContext.orElse(null);
216 this.filterConfig = filterConfig;
219 static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
220 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
221 return new InjectedFilterConfig(filter, servletContext, filterConfig);
225 public String getFilterName() {
230 public String getInitParameter(final String paramName) {
231 return filterConfig != null ? filterConfig.get(paramName) : null;
235 public Enumeration<String> getInitParameterNames() {
236 return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
237 : Collections.emptyEnumeration();
241 public ServletContext getServletContext() {
242 return servletContext;
247 * Extracts the custom filter list as provided by Karaf Configuration Admin.
249 * @return A <code>non-null</code> <code>List</code> of the custom filter
250 * fully qualified class names.
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();
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)));
266 return builder.build();
270 * Extract a subset of properties that apply to a particular Filter.
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.
280 private static Map<String, String> extractPropertiesForFilter(final String clazzName,
281 final Map<String, String> fullConfiguration) {
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());
294 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
295 LOG.error("couldn't parse property \"{}\"; skipping", key);
299 return extractedConfig;
303 * Register for config changes.
306 * A listener implementing
307 * <code>CustomFilterAdapterListener</code>
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);
318 private abstract static class FilterDTO {
320 private final Map<String, String> initParams;
322 protected FilterDTO(final Map<String, String> initParams) {
323 this.initParams = requireNonNull(initParams);
326 abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
328 static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
329 return new NamedFilterDTO(clazzName, initParams);
332 static FilterDTO createFilterDTO(final Filter instance) {
333 return new InstanceFilterDTO(instance);
337 * Attempts to extract a map of key/value pairs from a given file.
339 * @return map with the initialization parameters.
341 Map<String, String> getInitParams() {
347 * Essentially a tuple of (filterClassName, propertiesFileName). Allows
348 * quicker passing and return of Filter information.
350 private static class NamedFilterDTO extends FilterDTO {
351 private final String clazzName;
353 NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
355 this.clazzName = requireNonNull(clazzName);
358 @SuppressWarnings("unchecked")
360 Filter getInstance(final Optional<ServletContext> servletContext) {
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);
371 private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
373 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
375 filter.init(filterConfig);
376 } catch (ServletException e) {
377 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
384 public int hashCode() {
385 return clazzName.hashCode();
389 public boolean equals(final Object obj) {
398 if (getClass() != obj.getClass()) {
402 NamedFilterDTO other = (NamedFilterDTO) obj;
403 return clazzName.equals(other.clazzName);
407 public String toString() {
408 return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
412 private static class InstanceFilterDTO extends FilterDTO {
413 private final Filter instance;
415 InstanceFilterDTO(final Filter instance) {
416 super(Collections.emptyMap());
417 this.instance = requireNonNull(instance);
421 Filter getInstance(final Optional<ServletContext> servletContext) {
426 public int hashCode() {
427 return instance.hashCode();
431 public boolean equals(final Object obj) {
440 if (getClass() != obj.getClass()) {
444 InstanceFilterDTO other = (InstanceFilterDTO) obj;
445 return instance.equals(other.instance);
449 public String toString() {
450 return "InstanceFilterDTO [instance=" + instance + "]";