summaryrefslogtreecommitdiffstats
path: root/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
blob: b8b0bc8b60723f445229aaae2809be638aa9c716 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.httpd;

import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.StopPluginListener;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.ServletModule;
import java.io.IOException;
import java.util.Iterator;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/** Filters all HTTP requests passing through the server. */
public abstract class AllRequestFilter implements Filter {
  public static ServletModule module() {
    return new ServletModule() {
      @Override
      protected void configureServlets() {
        DynamicSet.setOf(binder(), AllRequestFilter.class);
        filter("/*").through(FilterProxy.class);

        bind(StopPluginListener.class)
            .annotatedWith(UniqueAnnotations.create())
            .to(FilterProxy.class);
      }
    };
  }

  @Singleton
  static class FilterProxy implements Filter, StopPluginListener {
    private final DynamicSet<AllRequestFilter> filters;

    private DynamicSet<AllRequestFilter> initializedFilters;
    private FilterConfig filterConfig;

    @Inject
    FilterProxy(DynamicSet<AllRequestFilter> filters) {
      this.filters = filters;
      this.initializedFilters = new DynamicSet<>();
      this.filterConfig = null;
    }

    /**
     * Initializes a filter if needed
     *
     * @param filter The filter that should get initialized
     * @return {@code true} iff filter is now initialized
     * @throws ServletException if filter itself fails to init
     */
    private synchronized boolean initFilterIfNeeded(AllRequestFilter filter)
        throws ServletException {
      boolean ret = true;
      if (filters.contains(filter)) {
        // Regardless of whether or not the caller checked filter's
        // containment in initializedFilters, we better re-check as we're now
        // synchronized.
        if (!initializedFilters.contains(filter)) {
          filter.init(filterConfig);
          initializedFilters.add(filter);
        }
      } else {
        ret = false;
      }
      return ret;
    }

    private synchronized void cleanUpInitializedFilters() {
      Iterable<AllRequestFilter> filtersToCleanUp = initializedFilters;
      initializedFilters = new DynamicSet<>();
      for (AllRequestFilter filter : filtersToCleanUp) {
        if (filters.contains(filter)) {
          initializedFilters.add(filter);
        } else {
          filter.destroy();
        }
      }
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain last)
        throws IOException, ServletException {
      final Iterator<AllRequestFilter> itr = filters.iterator();
      new FilterChain() {
        @Override
        public void doFilter(ServletRequest req, ServletResponse res)
            throws IOException, ServletException {
          while (itr.hasNext()) {
            AllRequestFilter filter = itr.next();
            // To avoid {@code synchronized} on the whole filtering (and
            // thereby killing concurrency), we start the below disjunction
            // with an unsynchronized check for containment. This
            // unsynchronized check is always correct if no filters got
            // initialized/cleaned concurrently behind our back.
            // The case of concurrently initialized filters is saved by the
            // call to initFilterIfNeeded. So that's fine too.
            // The case of concurrently cleaned filters between the {@code if}
            // condition and the call to {@code doFilter} is not saved by
            // anything. If a filter is getting removed concurrently while
            // another thread is in those two lines, doFilter might (but need
            // not) fail.
            //
            // Since this failure only occurs if a filter is deleted
            // (e.g.: a plugin reloaded) exactly when a thread is in those
            // two lines, and it only breaks a single request, we're ok with
            // it, given that this is really both really improbable and also
            // the "proper" fix for it would basically kill concurrency of
            // webrequests.
            if (initializedFilters.contains(filter) || initFilterIfNeeded(filter)) {
              filter.doFilter(req, res, this);
              return;
            }
          }
          last.doFilter(req, res);
        }
      }.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig config) throws ServletException {
      // Plugins that provide AllRequestFilters might get loaded later at
      // runtime, long after this init method had been called. To allow to
      // correctly init such plugins' AllRequestFilters, we keep the
      // FilterConfig around, and reuse it to lazy init the AllRequestFilters.
      filterConfig = config;

      for (AllRequestFilter f : filters) {
        initFilterIfNeeded(f);
      }
    }

    @Override
    public synchronized void destroy() {
      Iterable<AllRequestFilter> filtersToDestroy = initializedFilters;
      initializedFilters = new DynamicSet<>();
      for (AllRequestFilter filter : filtersToDestroy) {
        filter.destroy();
      }
    }

    @Override
    public void onStopPlugin(Plugin plugin) {
      // In order to allow properly garbage collection, we need to scrub
      // initializedFilters clean of filters stemming from plugins as they
      // get unloaded.
      cleanUpInitializedFilters();
    }
  }

  @Override
  public void init(FilterConfig config) throws ServletException {}

  @Override
  public void destroy() {}
}