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.Arrays;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.Enumeration;
19 import java.util.HashMap;
20 import java.util.List;
22 import java.util.Map.Entry;
23 import java.util.Optional;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.stream.Collectors;
26 import java.util.stream.Stream;
27 import javax.servlet.Filter;
28 import javax.servlet.FilterConfig;
29 import javax.servlet.ServletContext;
30 import javax.servlet.ServletException;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterConfiguration;
33 import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterListener;
34 import org.osgi.service.component.annotations.Activate;
35 import org.osgi.service.component.annotations.Component;
36 import org.osgi.service.component.annotations.Modified;
37 import org.osgi.service.component.annotations.Reference;
38 import org.osgi.service.component.annotations.ReferenceCardinality;
39 import org.osgi.service.component.annotations.ReferencePolicy;
40 import org.osgi.service.component.annotations.ReferencePolicyOption;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * Implementation of CustomFilterAdapterConfiguration.
47 @Component(immediate = true, configurationPid = "org.opendaylight.aaa.filterchain")
48 public final class CustomFilterAdapterConfigurationImpl implements CustomFilterAdapterConfiguration {
49 private static final Logger LOG = LoggerFactory.getLogger(CustomFilterAdapterConfigurationImpl.class);
52 * Separates different filter definitions. For example:
53 * <code>customFilterList = c.b.a.TestFilter1,f.d.e.TestFilter2,j.h.i.FilterN</code>
55 private static final String FILTER_DTO_SEPARATOR = ",";
58 * <code>customFilterList</code> is the property advertised in the Karaf
59 * configuration admin.
61 static final String CUSTOM_FILTER_LIST_KEY = "customFilterList";
64 * List of listeners to notify upon config admin events.
66 private final Collection<CustomFilterAdapterListener> listeners = ConcurrentHashMap.newKeySet();
69 * Saves a local copy of the most recent configuration so when a listener is
70 * added, it can receive and initial update.
72 private volatile List<FilterDTO> namedFilterDTOs = Collections.emptyList();
74 private volatile List<FilterDTO> instanceFilterDTOs = Collections.emptyList();
77 void activate(final Map<String, String> properties) {
82 // Invoked in response to configuration admin changes
83 public void update(final Map<String, String> properties) {
84 if (properties != null) {
85 LOG.info("Custom filter properties updated: {}", properties);
87 this.namedFilterDTOs = getCustomFilterList(properties);
92 // Invoked when a Filter OSGi service is added
93 @Reference(cardinality = ReferenceCardinality.MULTIPLE,
94 policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY,
95 // Needed to exclude any filters that is published for HTTP Whiteboard
96 // FIXME: it would be much better if we had a whitelist property to prevent confusion
98 + "(osgi.http.whiteboard.filter.pattern=*)"
99 + "(osgi.http.whiteboard.filter.regex=*)"
100 + "(osgi.http.whiteboard.filter.servlet=*)"
102 public void addFilter(final Filter filter) {
103 if (filter == null) {
107 LOG.info("Custom Filter {} added", filter);
108 this.instanceFilterDTOs = ImmutableList.<FilterDTO>builder().addAll(instanceFilterDTOs)
109 .add(FilterDTO.createFilterDTO(filter)).build();
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 this.instanceFilterDTOs = ImmutableList.copyOf(instanceFilterDTOs.stream().filter(dto -> !dto.equals(toRemove))
122 .collect(Collectors.toList()));
127 * Notify all listeners of a change event.
129 private void updateListeners() {
130 for (CustomFilterAdapterListener listener : listeners) {
131 updateListener(listener);
136 * Update a particular listener with the new injected <code>FilterDTO</code>
140 * The <code>CustomFilterAdapter</code> instance
141 * @param customFilterList
142 * The newly injected <code>FilterDTO</code> list
144 private void updateListener(final CustomFilterAdapterListener listener) {
145 final Optional<ServletContext> listenerServletContext = extractServletContext(listener);
146 final List<Filter> filterList = convertCustomFilterList(listenerServletContext);
148 LOG.debug("Notifying listener {} of filters {}", listener, filterList);
150 listener.updateInjectedFilters(filterList);
154 * Utility method to extract a <code>ServletContext</code> from a listener's
155 * <code>FilterConfig</code>.
158 * An object which listens for filter chain configuration
160 * @return An extracted <code>ServletContext</code>, or null if either the
161 * <code>FilterConfig</code> of <code>ServletContext</code> is null
163 private static Optional<ServletContext> extractServletContext(final CustomFilterAdapterListener listener) {
164 final FilterConfig listenerFilterConfig = listener.getFilterConfig();
165 return listenerFilterConfig != null ? Optional.ofNullable(listenerFilterConfig.getServletContext())
170 * Converts a List of class names (possibly Filters) and attempts to spawn
171 * corresponding <code>javax.servlet.Filter</code> instances.
173 * @param customFilterList
174 * a list of class names, ideally Filters
175 * @return a list of derived Filter(s)
177 private List<Filter> convertCustomFilterList(final Optional<ServletContext> listenerServletContext) {
178 final List<Filter> filterList = ImmutableList.<FilterDTO>builder().addAll(namedFilterDTOs)
179 .addAll(instanceFilterDTOs).build().stream().flatMap(
180 filter -> getFilterInstance(filter, listenerServletContext)).collect(Collectors.toList());
181 return Collections.unmodifiableList(filterList);
185 * Utility method used to create and initialize a Filter from a FilterDTO.
187 * @param customFilter
188 * DTO containing Filter and properties path, if one exists.
189 * @param servletContext
190 * Scoped to the listener
191 * @return A Stream containing the Filter, or empty if one cannot be instantiated.
193 private static Stream<Filter> getFilterInstance(final FilterDTO customFilter,
194 final Optional<ServletContext> servletContext) {
195 final Filter filter = customFilter.getInstance(servletContext);
196 if (filter != null) {
197 LOG.info("Successfully loaded custom Filter {} for context {}", filter, servletContext);
198 return Stream.of(filter);
201 return Stream.empty();
205 * Allows creation of <code>FilterConfig</code> from a key/value properties file.
207 private static final class InjectedFilterConfig implements FilterConfig {
209 private final String filterName;
210 private final ServletContext servletContext;
211 private final Map<String, String> filterConfig;
213 // private for Factory Method pattern
214 private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
215 final Map<String, String> filterConfig) {
217 this.filterName = filter.getClass().getSimpleName();
218 this.servletContext = servletContext.orElse(null);
219 this.filterConfig = filterConfig;
222 static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
223 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
224 return new InjectedFilterConfig(filter, servletContext, filterConfig);
228 public String getFilterName() {
233 public String getInitParameter(final String paramName) {
234 return filterConfig != null ? filterConfig.get(paramName) : null;
238 public Enumeration<String> getInitParameterNames() {
239 return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
240 : Collections.emptyEnumeration();
244 public ServletContext getServletContext() {
245 return servletContext;
250 * Extracts the custom filter list as provided by Karaf Configuration Admin.
252 * @return A <code>non-null</code> <code>List</code> of the custom filter
253 * fully qualified class names.
255 private static List<FilterDTO> getCustomFilterList(final Map<String, String> configuration) {
256 final String customFilterListValue = configuration.get(CUSTOM_FILTER_LIST_KEY);
257 final ImmutableList.Builder<FilterDTO> customFilterListBuilder = ImmutableList.builder();
258 if (customFilterListValue != null) {
259 // Creates the list from comma separate values; whitespace is
261 final List<String> filterClazzNames = Arrays
262 .asList(customFilterListValue.replaceAll("\\s", "").split(FILTER_DTO_SEPARATOR));
263 for (String filterClazzName : filterClazzNames) {
264 if (!Strings.isNullOrEmpty(filterClazzName)) {
265 final Map<String, String> applicableConfigs = extractPropertiesForFilter(filterClazzName,
267 final FilterDTO filterDTO = FilterDTO.createFilterDTO(filterClazzName, applicableConfigs);
268 customFilterListBuilder.add(filterDTO);
272 return customFilterListBuilder.build();
276 * Extract a subset of properties that apply to a particular Filter.
279 * prefix used to specify key value pair (i.e.,
280 * a.b.c.Filter.property)
281 * @param fullConfiguration
282 * The entire configuration dictionary, which is traversed for
283 * applicable properties.
284 * @return A Map of applicable properties for a filter.
286 private static Map<String, String> extractPropertiesForFilter(final String clazzName,
287 final Map<String, String> fullConfiguration) {
289 final Map<String, String> extractedConfig = new HashMap<>();
290 for (Entry<String, String> entry : fullConfiguration.entrySet()) {
291 String key = entry.getKey();
292 final int lastDotSeparator = key.lastIndexOf(".");
293 if (lastDotSeparator >= 0) {
294 final String comparisonClazzNameSubstring = key.substring(0, lastDotSeparator);
295 if (comparisonClazzNameSubstring.equals(clazzName)) {
296 final String filterInitParamKey = key.substring(lastDotSeparator + 1);
297 extractedConfig.put(filterInitParamKey, entry.getValue());
300 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
301 LOG.error("couldn't parse property \"{}\"; skipping", key);
305 return extractedConfig;
309 * Register for config changes.
312 * A listener implementing
313 * <code>CustomFilterAdapterListener</code>
316 public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
317 LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
318 if (this.listeners.add(listener)) {
319 LOG.debug("Updated listener set: {}", listeners);
320 this.updateListener(listener);
324 private abstract static class FilterDTO {
326 private final Map<String, String> initParams;
328 protected FilterDTO(final Map<String, String> initParams) {
329 this.initParams = requireNonNull(initParams);
332 abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
334 static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
335 return new NamedFilterDTO(clazzName, initParams);
338 static FilterDTO createFilterDTO(final Filter instance) {
339 return new InstanceFilterDTO(instance);
343 * Attempts to extract a map of key/value pairs from a given file.
345 * @return map with the initialization parameters.
347 Map<String, String> getInitParams() {
353 * Essentially a tuple of (filterClassName, propertiesFileName). Allows
354 * quicker passing and return of Filter information.
356 private static class NamedFilterDTO extends FilterDTO {
357 private final String clazzName;
359 NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
361 this.clazzName = requireNonNull(clazzName);
364 @SuppressWarnings("unchecked")
366 Filter getInstance(final Optional<ServletContext> servletContext) {
368 final Class<Filter> filterClazz = (Class<Filter>) Class.forName(clazzName);
369 return init(filterClazz.getDeclaredConstructor().newInstance(), servletContext);
370 } catch (ReflectiveOperationException | ClassCastException e) {
371 LOG.error("Error loading {}", this, e);
377 private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
379 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
381 filter.init(filterConfig);
382 } catch (ServletException e) {
383 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
390 public int hashCode() {
391 return clazzName.hashCode();
395 public boolean equals(final Object obj) {
404 if (getClass() != obj.getClass()) {
408 NamedFilterDTO other = (NamedFilterDTO) obj;
409 return clazzName.equals(other.clazzName);
413 public String toString() {
414 return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
418 private static class InstanceFilterDTO extends FilterDTO {
419 private final Filter instance;
421 InstanceFilterDTO(final Filter instance) {
422 super(Collections.emptyMap());
423 this.instance = requireNonNull(instance);
427 Filter getInstance(final Optional<ServletContext> servletContext) {
432 public int hashCode() {
433 return instance.hashCode();
437 public boolean equals(final Object obj) {
446 if (getClass() != obj.getClass()) {
450 InstanceFilterDTO other = (InstanceFilterDTO) obj;
451 return instance.equals(other.instance);
455 public String toString() {
456 return "InstanceFilterDTO [instance=" + instance + "]";