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.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;
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(new InstanceFilterDTO(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 var toRemove = new InstanceFilterDTO(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 .map(filter -> getFilterInstance(filter, listenerServletContext))
178 .filter(Objects::nonNull)
179 .collect(ImmutableList.toImmutableList());
183 * Utility method used to create and initialize a Filter from a FilterDTO.
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
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);
199 * Allows creation of <code>FilterConfig</code> from a key/value properties file.
201 private static final class InjectedFilterConfig implements FilterConfig {
203 private final String filterName;
204 private final ServletContext servletContext;
205 private final Map<String, String> filterConfig;
207 // private for Factory Method pattern
208 private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
209 final Map<String, String> filterConfig) {
211 filterName = filter.getClass().getSimpleName();
212 this.servletContext = servletContext.orElse(null);
213 this.filterConfig = filterConfig;
216 static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
217 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
218 return new InjectedFilterConfig(filter, servletContext, filterConfig);
222 public String getFilterName() {
227 public String getInitParameter(final String paramName) {
228 return filterConfig != null ? filterConfig.get(paramName) : null;
232 public Enumeration<String> getInitParameterNames() {
233 return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
234 : Collections.emptyEnumeration();
238 public ServletContext getServletContext() {
239 return servletContext;
244 * Extracts the custom filter list as provided by Karaf Configuration Admin.
246 * @return A <code>non-null</code> <code>List</code> of the custom filter
247 * fully qualified class names.
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();
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(new NamedFilterDTO(filterClazzName,
260 extractPropertiesForFilter(filterClazzName, configuration)));
263 return builder.build();
267 * Extract a subset of properties that apply to a particular Filter.
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.
277 private static Map<String, String> extractPropertiesForFilter(final String clazzName,
278 final Map<String, String> fullConfiguration) {
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());
290 } else if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
291 LOG.error("couldn't parse property \"{}\"; skipping", key);
294 return extractedConfig;
298 * Register for config changes.
301 * A listener implementing
302 * <code>CustomFilterAdapterListener</code>
305 public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
306 LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
307 if (listeners.add(listener)) {
308 LOG.debug("Updated listener set: {}", listeners);
309 updateListener(listener);
313 private abstract static sealed class FilterDTO {
315 abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
318 public abstract int hashCode();
321 public abstract boolean equals(Object obj);
324 public abstract String toString();
328 * Essentially a tuple of (filterClassName, propertiesFileName). Allows
329 * quicker passing and return of Filter information.
331 private static final class NamedFilterDTO extends FilterDTO {
332 private final Map<String, String> initParams;
333 private final String clazzName;
335 NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
336 this.clazzName = requireNonNull(clazzName);
337 this.initParams = requireNonNull(initParams);
341 Filter getInstance(final Optional<ServletContext> servletContext) {
342 final Filter instance;
344 instance = Class.forName(clazzName).asSubclass(Filter.class).getDeclaredConstructor().newInstance();
345 } catch (ReflectiveOperationException | ClassCastException e) {
346 LOG.error("Error loading {}", this, e);
349 return init(instance, servletContext);
352 private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
354 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
356 filter.init(filterConfig);
357 } catch (ServletException e) {
358 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
365 public int hashCode() {
366 return clazzName.hashCode();
370 public boolean equals(final Object obj) {
371 return this == obj || obj instanceof NamedFilterDTO other && clazzName.equals(other.clazzName);
375 public String toString() {
376 return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + initParams + "]";
380 private static final class InstanceFilterDTO extends FilterDTO {
381 private final Filter instance;
383 InstanceFilterDTO(final Filter instance) {
384 this.instance = requireNonNull(instance);
388 Filter getInstance(final Optional<ServletContext> servletContext) {
393 public int hashCode() {
394 return instance.hashCode();
398 public boolean equals(final Object obj) {
399 return this == obj || obj instanceof InstanceFilterDTO other && instance.equals(other.instance);
403 public String toString() {
404 return "InstanceFilterDTO [instance=" + instance + "]";