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(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 .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 this.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(FilterDTO.createFilterDTO(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());
291 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
292 LOG.error("couldn't parse property \"{}\"; skipping", key);
296 return extractedConfig;
300 * Register for config changes.
303 * A listener implementing
304 * <code>CustomFilterAdapterListener</code>
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);
315 private abstract static class FilterDTO {
317 private final Map<String, String> initParams;
319 protected FilterDTO(final Map<String, String> initParams) {
320 this.initParams = requireNonNull(initParams);
323 abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
325 static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
326 return new NamedFilterDTO(clazzName, initParams);
329 static FilterDTO createFilterDTO(final Filter instance) {
330 return new InstanceFilterDTO(instance);
334 * Attempts to extract a map of key/value pairs from a given file.
336 * @return map with the initialization parameters.
338 Map<String, String> getInitParams() {
344 * Essentially a tuple of (filterClassName, propertiesFileName). Allows
345 * quicker passing and return of Filter information.
347 private static class NamedFilterDTO extends FilterDTO {
348 private final String clazzName;
350 NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
352 this.clazzName = requireNonNull(clazzName);
355 @SuppressWarnings("unchecked")
357 Filter getInstance(final Optional<ServletContext> servletContext) {
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);
368 private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
370 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
372 filter.init(filterConfig);
373 } catch (ServletException e) {
374 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
381 public int hashCode() {
382 return clazzName.hashCode();
386 public boolean equals(final Object obj) {
395 if (getClass() != obj.getClass()) {
399 NamedFilterDTO other = (NamedFilterDTO) obj;
400 return clazzName.equals(other.clazzName);
404 public String toString() {
405 return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
409 private static class InstanceFilterDTO extends FilterDTO {
410 private final Filter instance;
412 InstanceFilterDTO(final Filter instance) {
413 super(Collections.emptyMap());
414 this.instance = requireNonNull(instance);
418 Filter getInstance(final Optional<ServletContext> servletContext) {
423 public int hashCode() {
424 return instance.hashCode();
428 public boolean equals(final Object obj) {
437 if (getClass() != obj.getClass()) {
441 InstanceFilterDTO other = (InstanceFilterDTO) obj;
442 return instance.equals(other.instance);
446 public String toString() {
447 return "InstanceFilterDTO [instance=" + instance + "]";