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 List<FilterDTO> namedFilterDTOs = Collections.emptyList();
73 private volatile List<FilterDTO> instanceFilterDTOs = Collections.emptyList();
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 this.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 this.instanceFilterDTOs = ImmutableList.<FilterDTO>builder().addAll(instanceFilterDTOs)
108 .add(FilterDTO.createFilterDTO(filter)).build();
112 // Invoked when a Filter OSGi service is removed
113 public void removeFilter(final Filter filter) {
114 if (filter == null) {
118 LOG.info("Custom Filter {} removed", filter);
119 FilterDTO toRemove = FilterDTO.createFilterDTO(filter);
120 this.instanceFilterDTOs = ImmutableList.copyOf(instanceFilterDTOs.stream().filter(dto -> !dto.equals(toRemove))
121 .collect(Collectors.toList()));
126 * Notify all listeners of a change event.
128 private void updateListeners() {
129 for (CustomFilterAdapterListener listener : listeners) {
130 updateListener(listener);
135 * Update a particular listener with the new injected <code>FilterDTO</code>
139 * The <code>CustomFilterAdapter</code> instance
140 * @param customFilterList
141 * The newly injected <code>FilterDTO</code> list
143 private void updateListener(final CustomFilterAdapterListener listener) {
144 final Optional<ServletContext> listenerServletContext = extractServletContext(listener);
145 final List<Filter> filterList = convertCustomFilterList(listenerServletContext);
147 LOG.debug("Notifying listener {} of filters {}", listener, filterList);
149 listener.updateInjectedFilters(filterList);
153 * Utility method to extract a <code>ServletContext</code> from a listener's
154 * <code>FilterConfig</code>.
157 * An object which listens for filter chain configuration
159 * @return An extracted <code>ServletContext</code>, or null if either the
160 * <code>FilterConfig</code> of <code>ServletContext</code> is null
162 private static Optional<ServletContext> extractServletContext(final CustomFilterAdapterListener listener) {
163 final FilterConfig listenerFilterConfig = listener.getFilterConfig();
164 return listenerFilterConfig != null ? Optional.ofNullable(listenerFilterConfig.getServletContext())
169 * Converts a List of class names (possibly Filters) and attempts to spawn
170 * corresponding <code>javax.servlet.Filter</code> instances.
172 * @param customFilterList
173 * a list of class names, ideally Filters
174 * @return a list of derived Filter(s)
176 private List<Filter> convertCustomFilterList(final Optional<ServletContext> listenerServletContext) {
177 final List<Filter> filterList = ImmutableList.<FilterDTO>builder().addAll(namedFilterDTOs)
178 .addAll(instanceFilterDTOs).build().stream().flatMap(
179 filter -> getFilterInstance(filter, listenerServletContext)).collect(Collectors.toList());
180 return Collections.unmodifiableList(filterList);
184 * Utility method used to create and initialize a Filter from a FilterDTO.
186 * @param customFilter
187 * DTO containing Filter and properties path, if one exists.
188 * @param servletContext
189 * Scoped to the listener
190 * @return A Stream containing the Filter, or empty if one cannot be instantiated.
192 private static Stream<Filter> getFilterInstance(final FilterDTO customFilter,
193 final Optional<ServletContext> servletContext) {
194 final Filter filter = customFilter.getInstance(servletContext);
195 if (filter != null) {
196 LOG.info("Successfully loaded custom Filter {} for context {}", filter, servletContext);
197 return Stream.of(filter);
200 return Stream.empty();
204 * Allows creation of <code>FilterConfig</code> from a key/value properties file.
206 private static final class InjectedFilterConfig implements FilterConfig {
208 private final String filterName;
209 private final ServletContext servletContext;
210 private final Map<String, String> filterConfig;
212 // private for Factory Method pattern
213 private InjectedFilterConfig(final Filter filter, final Optional<ServletContext> servletContext,
214 final Map<String, String> filterConfig) {
216 this.filterName = filter.getClass().getSimpleName();
217 this.servletContext = servletContext.orElse(null);
218 this.filterConfig = filterConfig;
221 static InjectedFilterConfig createInjectedFilterConfig(final Filter filter,
222 final Optional<ServletContext> servletContext, final Map<String, String> filterConfig) {
223 return new InjectedFilterConfig(filter, servletContext, filterConfig);
227 public String getFilterName() {
232 public String getInitParameter(final String paramName) {
233 return filterConfig != null ? filterConfig.get(paramName) : null;
237 public Enumeration<String> getInitParameterNames() {
238 return filterConfig != null ? Iterators.asEnumeration(filterConfig.keySet().iterator())
239 : Collections.emptyEnumeration();
243 public ServletContext getServletContext() {
244 return servletContext;
249 * Extracts the custom filter list as provided by Karaf Configuration Admin.
251 * @return A <code>non-null</code> <code>List</code> of the custom filter
252 * fully qualified class names.
254 private static List<FilterDTO> getCustomFilterList(final Map<String, String> configuration) {
255 final String customFilterListValue = configuration.get(CUSTOM_FILTER_LIST_KEY);
256 final ImmutableList.Builder<FilterDTO> customFilterListBuilder = ImmutableList.builder();
257 if (customFilterListValue != null) {
258 // Creates the list from comma separate values; whitespace is removed first
259 for (String filterClazzName : customFilterListValue.replaceAll("\\s", "").split(FILTER_DTO_SEPARATOR)) {
260 if (!Strings.isNullOrEmpty(filterClazzName)) {
261 final Map<String, String> applicableConfigs = extractPropertiesForFilter(filterClazzName,
263 final FilterDTO filterDTO = FilterDTO.createFilterDTO(filterClazzName, applicableConfigs);
264 customFilterListBuilder.add(filterDTO);
268 return customFilterListBuilder.build();
272 * Extract a subset of properties that apply to a particular Filter.
275 * prefix used to specify key value pair (i.e.,
276 * a.b.c.Filter.property)
277 * @param fullConfiguration
278 * The entire configuration dictionary, which is traversed for
279 * applicable properties.
280 * @return A Map of applicable properties for a filter.
282 private static Map<String, String> extractPropertiesForFilter(final String clazzName,
283 final Map<String, String> fullConfiguration) {
285 final Map<String, String> extractedConfig = new HashMap<>();
286 for (Entry<String, String> entry : fullConfiguration.entrySet()) {
287 String key = entry.getKey();
288 final int lastDotSeparator = key.lastIndexOf(".");
289 if (lastDotSeparator >= 0) {
290 final String comparisonClazzNameSubstring = key.substring(0, lastDotSeparator);
291 if (comparisonClazzNameSubstring.equals(clazzName)) {
292 final String filterInitParamKey = key.substring(lastDotSeparator + 1);
293 extractedConfig.put(filterInitParamKey, entry.getValue());
296 if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
297 LOG.error("couldn't parse property \"{}\"; skipping", key);
301 return extractedConfig;
305 * Register for config changes.
308 * A listener implementing
309 * <code>CustomFilterAdapterListener</code>
312 public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
313 LOG.debug("registerCustomFilterAdapterConfigurationListener: {}", listener);
314 if (this.listeners.add(listener)) {
315 LOG.debug("Updated listener set: {}", listeners);
316 this.updateListener(listener);
320 private abstract static class FilterDTO {
322 private final Map<String, String> initParams;
324 protected FilterDTO(final Map<String, String> initParams) {
325 this.initParams = requireNonNull(initParams);
328 abstract @Nullable Filter getInstance(Optional<ServletContext> servletContext);
330 static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
331 return new NamedFilterDTO(clazzName, initParams);
334 static FilterDTO createFilterDTO(final Filter instance) {
335 return new InstanceFilterDTO(instance);
339 * Attempts to extract a map of key/value pairs from a given file.
341 * @return map with the initialization parameters.
343 Map<String, String> getInitParams() {
349 * Essentially a tuple of (filterClassName, propertiesFileName). Allows
350 * quicker passing and return of Filter information.
352 private static class NamedFilterDTO extends FilterDTO {
353 private final String clazzName;
355 NamedFilterDTO(final String clazzName, final Map<String, String> initParams) {
357 this.clazzName = requireNonNull(clazzName);
360 @SuppressWarnings("unchecked")
362 Filter getInstance(final Optional<ServletContext> servletContext) {
364 final Class<Filter> filterClazz = (Class<Filter>) Class.forName(clazzName);
365 return init(filterClazz.getDeclaredConstructor().newInstance(), servletContext);
366 } catch (ReflectiveOperationException | ClassCastException e) {
367 LOG.error("Error loading {}", this, e);
373 private Filter init(final Filter filter, final Optional<ServletContext> servletContext) {
375 FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext,
377 filter.init(filterConfig);
378 } catch (ServletException e) {
379 LOG.error("Error injecting custom filter {} - continuing anyway", filter, e);
386 public int hashCode() {
387 return clazzName.hashCode();
391 public boolean equals(final Object obj) {
400 if (getClass() != obj.getClass()) {
404 NamedFilterDTO other = (NamedFilterDTO) obj;
405 return clazzName.equals(other.clazzName);
409 public String toString() {
410 return "NamedFilterDTO [clazzName=" + clazzName + ", initParams=" + getInitParams() + "]";
414 private static class InstanceFilterDTO extends FilterDTO {
415 private final Filter instance;
417 InstanceFilterDTO(final Filter instance) {
418 super(Collections.emptyMap());
419 this.instance = requireNonNull(instance);
423 Filter getInstance(final Optional<ServletContext> servletContext) {
428 public int hashCode() {
429 return instance.hashCode();
433 public boolean equals(final Object obj) {
442 if (getClass() != obj.getClass()) {
446 InstanceFilterDTO other = (InstanceFilterDTO) obj;
447 return instance.equals(other.instance);
451 public String toString() {
452 return "InstanceFilterDTO [instance=" + instance + "]";