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 java.util.Collection;
16 import java.util.Collections;
17 import java.util.Enumeration;
18 import java.util.HashMap;
19 import java.util.List;
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;
44 * Implementation of CustomFilterAdapterConfiguration.
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);
51 * Separates different filter definitions. For example:
52 * <code>customFilterList = c.b.a.TestFilter1,f.d.e.TestFilter2,j.h.i.FilterN</code>
54 private static final String FILTER_DTO_SEPARATOR = ",";
57 * <code>customFilterList</code> is the property advertised in the Karaf
58 * configuration admin.
60 static final String CUSTOM_FILTER_LIST_KEY = "customFilterList";
63 * List of listeners to notify upon config admin events.
65 private final Collection<CustomFilterAdapterListener> listeners = ConcurrentHashMap.newKeySet();
68 * Saves a local copy of the most recent configuration so when a listener is
69 * added, it can receive and initial update.
71 private volatile ImmutableList<FilterDTO> namedFilterDTOs = ImmutableList.of();
73 private volatile ImmutableList<FilterDTO> instanceFilterDTOs = ImmutableList.of();
76 void activate(final Map<String, String> properties) {
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);
86 namedFilterDTOs = getCustomFilterList(properties);
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
97 + "(osgi.http.whiteboard.filter.pattern=*)"
98 + "(osgi.http.whiteboard.filter.regex=*)"
99 + "(osgi.http.whiteboard.filter.servlet=*)"
101 public void addFilter(final Filter filter) {
102 if (filter == null) {
106 LOG.info("Custom Filter {} added", filter);
107 instanceFilterDTOs = ImmutableList.<FilterDTO>builder()
108 .addAll(instanceFilterDTOs)
109 .add(FilterDTO.createFilterDTO(filter))
114 // Invoked when a Filter OSGi service is removed
115 public void removeFilter(final Filter filter) {
116 if (filter == null) {
120 LOG.info("Custom Filter {} removed", filter);
121 FilterDTO toRemove = FilterDTO.createFilterDTO(filter);
122 instanceFilterDTOs = instanceFilterDTOs.stream()
123 .filter(dto -> !dto.equals(toRemove))
124 .collect(ImmutableList.toImmutableList());
129 * Notify all listeners of a change event.
131 private void updateListeners() {
132 for (CustomFilterAdapterListener listener : listeners) {
133 updateListener(listener);
138 * Update a particular listener with the new injected <code>FilterDTO</code>
142 * The <code>CustomFilterAdapter</code> instance
143 * @param customFilterList
144 * The newly injected <code>FilterDTO</code> list
146 private void updateListener(final CustomFilterAdapterListener listener) {
147 final Optional<ServletContext> listenerServletContext = extractServletContext(listener);
148 final List<Filter> filterList = convertCustomFilterList(listenerServletContext);
150 LOG.debug("Notifying listener {} of filters {}", listener, filterList);
152 listener.updateInjectedFilters(filterList);
156 * Utility method to extract a <code>ServletContext</code> from a listener's
157 * <code>FilterConfig</code>.
160 * An object which listens for filter chain configuration
162 * @return An extracted <code>ServletContext</code>, or null if either the
163 * <code>FilterConfig</code> of <code>ServletContext</code> is null
165 private static Optional<ServletContext> extractServletContext(final CustomFilterAdapterListener listener) {
166 final FilterConfig listenerFilterConfig = listener.getFilterConfig();
167 return listenerFilterConfig != null ? Optional.ofNullable(listenerFilterConfig.getServletContext())
172 * Converts a List of class names (possibly Filters) and attempts to spawn
173 * corresponding <code>javax.servlet.Filter</code> instances.
175 * @param customFilterList
176 * a list of class names, ideally Filters
177 * @return a list of derived Filter(s)
179 private List<Filter> convertCustomFilterList(final Optional<ServletContext> listenerServletContext) {
180 final List<Filter> filterList = ImmutableList.<FilterDTO>builder().addAll(namedFilterDTOs)
181 .addAll(instanceFilterDTOs).build().stream().flatMap(
182 filter -> getFilterInstance(filter, listenerServletContext)).collect(Collectors.toList());
183 return Collections.unmodifiableList(filterList);
187 * Utility method used to create and initialize a Filter from a FilterDTO.
189 * @param customFilter
190 * DTO containing Filter and properties path, if one exists.
191 * @param servletContext
192 * Scoped to the listener
193 * @return A Stream containing the Filter, or empty if one cannot be instantiated.
195 private static Stream<Filter> getFilterInstance(final FilterDTO customFilter,
196 final Optional<ServletContext> servletContext) {
197 final Filter filter = customFilter.getInstance(servletContext);
198 if (filter != null) {
199 LOG.info("Successfully loaded custom Filter {} for context {}", filter, servletContext);
200 return Stream.of(filter);
203 return Stream.empty();
207 * Allows creation of <code>FilterConfig</code> from a key/value properties file.
209 private static final class InjectedFilterConfig implements FilterConfig {
211 private final String filterName;
212 private final ServletContext servletContext;
213 private final Map<String, String> filterConfig;
215 // private for Factory Method pattern
216 private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
217 final Map<String, String> filterConfig) {
219 this.filterName = filter.getClass().getSimpleName();
220 this.servletContext = servletContext.orElse(null);
221 this.filterConfig = filterConfig;
224 static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
225 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
226 return new InjectedFilterConfig(filter, servletContext, filterConfig);
230 public String getFilterName() {
235 public String getInitParameter(final String paramName) {
236 return filterConfig != null ? filterConfig.get(paramName) : null;
240 public Enumeration<String> getInitParameterNames() {
241 return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
242 : Collections.emptyEnumeration();
246 public ServletContext getServletContext() {
247 return servletContext;
252 * Extracts the custom filter list as provided by Karaf Configuration Admin.
254 * @return A <code>non-null</code> <code>List</code> of the custom filter
255 * fully qualified class names.
257 private static ImmutableList<FilterDTO> getCustomFilterList(final Map<String, String> configuration) {
258 final var customFilterListValue = configuration.get(CUSTOM_FILTER_LIST_KEY);
259 if (customFilterListValue == null) {
260 return ImmutableList.of();
263 final var builder = ImmutableList.<FilterDTO>builder();
264 // Creates the list from comma separate values; whitespace is removed first
265 for (var filterClazzName : customFilterListValue.replaceAll("\\s", "").split(FILTER_DTO_SEPARATOR)) {
266 if (!Strings.isNullOrEmpty(filterClazzName)) {
267 builder.add(FilterDTO.createFilterDTO(filterClazzName,
268 extractPropertiesForFilter(filterClazzName, configuration)));
271 return builder.build();
275 * Extract a subset of properties that apply to a particular Filter.
278 * prefix used to specify key value pair (i.e.,
279 * a.b.c.Filter.property)
280 * @param fullConfiguration
281 * The entire configuration dictionary, which is traversed for
282 * applicable properties.
283 * @return A Map of applicable properties for a filter.
285 private static Map<String, String> extractPropertiesForFilter(final String clazzName,
286 final Map<String, String> fullConfiguration) {
288 final Map<String, String> extractedConfig = new HashMap<>();
289 for (Entry<String, String> entry : fullConfiguration.entrySet()) {
290 String key = entry.getKey();
291 final int lastDotSeparator = key.lastIndexOf(".");
292 if (lastDotSeparator >= 0) {
293 final String comparisonClazzNameSubstring = key.substring(0, lastDotSeparator);
294 if (comparisonClazzNameSubstring.equals(clazzName)) {
295 final String filterInitParamKey = key.substring(lastDotSeparator + 1);
296 extractedConfig.put(filterInitParamKey, entry.getValue());
299 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
300 LOG.error("couldn't parse property \"{}\"; skipping", key);
304 return extractedConfig;
308 * Register for config changes.
311 * A listener implementing
312 * <code>CustomFilterAdapterListener</code>
315 public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
316 LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
317 if (this.listeners.add(listener)) {
318 LOG.debug("Updated listener set: {}", listeners);
319 this.updateListener(listener);
323 private abstract static class FilterDTO {
325 private final Map<String, String> initParams;
327 protected FilterDTO(final Map<String, String> initParams) {
328 this.initParams = requireNonNull(initParams);
331 abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
333 static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
334 return new NamedFilterDTO(clazzName, initParams);
337 static FilterDTO createFilterDTO(final Filter instance) {
338 return new InstanceFilterDTO(instance);
342 * Attempts to extract a map of key/value pairs from a given file.
344 * @return map with the initialization parameters.
346 Map<String, String> getInitParams() {
352 * Essentially a tuple of (filterClassName, propertiesFileName). Allows
353 * quicker passing and return of Filter information.
355 private static class NamedFilterDTO extends FilterDTO {
356 private final String clazzName;
358 NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
360 this.clazzName = requireNonNull(clazzName);
363 @SuppressWarnings("unchecked")
365 Filter getInstance(final Optional<ServletContext> servletContext) {
367 final Class<Filter> filterClazz = (Class<Filter>) Class.forName(clazzName);
368 return init(filterClazz.getDeclaredConstructor().newInstance(), servletContext);
369 } catch (ReflectiveOperationException | ClassCastException e) {
370 LOG.error("Error loading {}", this, e);
376 private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
378 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
380 filter.init(filterConfig);
381 } catch (ServletException e) {
382 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
389 public int hashCode() {
390 return clazzName.hashCode();
394 public boolean equals(final Object obj) {
403 if (getClass() != obj.getClass()) {
407 NamedFilterDTO other = (NamedFilterDTO) obj;
408 return clazzName.equals(other.clazzName);
412 public String toString() {
413 return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
417 private static class InstanceFilterDTO extends FilterDTO {
418 private final Filter instance;
420 InstanceFilterDTO(final Filter instance) {
421 super(Collections.emptyMap());
422 this.instance = requireNonNull(instance);
426 Filter getInstance(final Optional<ServletContext> servletContext) {
431 public int hashCode() {
432 return instance.hashCode();
436 public boolean equals(final Object obj) {
445 if (getClass() != obj.getClass()) {
449 InstanceFilterDTO other = (InstanceFilterDTO) obj;
450 return instance.equals(other.instance);
454 public String toString() {
455 return "InstanceFilterDTO [instance=" + instance + "]";