001package org.javasimon.javaee;
002
003import javax.servlet.http.HttpServletRequest;
004
005import org.javasimon.Manager;
006import org.javasimon.Stopwatch;
007import org.javasimon.source.AbstractStopwatchSource;
008import org.javasimon.source.CachedStopwatchSource;
009import org.javasimon.source.StopwatchSource;
010import org.javasimon.utils.Replacer;
011
012/**
013 * Provide stopwatch source for HTTP Servlet request.
014 * Used by {@link SimonServletFilter} as default stopwatch source.
015 * Can be overridden to customize monitored HTTP Requests and their
016 * related Simon name.
017 * <p/>
018 * To select which HTTP Request should be monitored method {@link #isMonitored} can be overridden. Default implementation monitors everything except for
019 * typical resource-like requests (images, JS/CSS, ...).
020 *
021 * @author gquintana
022 * @author <a href="mailto:virgo47@gmail.com">Richard "Virgo" Richter</a>
023 */
024public class HttpStopwatchSource extends AbstractStopwatchSource<HttpServletRequest> {
025
026        /**
027         * Enum that represents modes of preserving HTTP methods names in Simons' names
028         */
029        public static enum IncludeHttpMethodName {
030                // Always append name of HTTP method to simon name
031                ALWAYS,
032                // Never append name of HTTP method to simon name
033                NEVER,
034                // Append name of all HTTP methods except GET method
035                NON_GET
036        }
037
038        /**
039         * Default prefix for web filter Simons if no "prefix" init parameter is used.
040         */
041        public static final String DEFAULT_SIMON_PREFIX = "org.javasimon.web";
042
043        /**
044         * Name of HTTP GET method.
045         */
046        private static final String GET_METHOD = "GET";
047
048        /**
049         * Simon prefix, can be set to {@code null}.
050         */
051        private String prefix = DEFAULT_SIMON_PREFIX;
052
053        private IncludeHttpMethodName includeHttpMethodName = IncludeHttpMethodName.NEVER;
054
055        private Replacer unallowedCharacterReplacer = SimonServletFilterUtils.createUnallowedCharsReplacer("_");
056        private Replacer jsessionParameterReplacer = new Replacer("[;&]?JSESSIONID=[^;?/&]*", "", Replacer.Modificator.IGNORE_CASE);
057        private Replacer trailingStuffReplacer = new Replacer("/[^a-zA-Z]*$", "");
058
059        public HttpStopwatchSource(Manager manager) {
060                super(manager);
061        }
062
063        public String getPrefix() {
064                return prefix;
065        }
066
067        public void setPrefix(String prefix) {
068                this.prefix = prefix;
069        }
070
071        public String getReplaceUnallowed() {
072                return unallowedCharacterReplacer.getTo();
073        }
074
075        public void setReplaceUnallowed(String replaceUnallowed) {
076                unallowedCharacterReplacer.setTo(replaceUnallowed);
077        }
078
079        /**
080         * Returns current mode of preserving HTTP method names in simons' names
081         *
082         * @return current mode of preserving HTTP method names
083         */
084        public IncludeHttpMethodName getIncludeHttpMethodName() {
085                return includeHttpMethodName;
086        }
087
088        /**
089         *  Set current mode of preserving HTTP method names in simons' names
090         *
091         * @param includeHttpMethodName current mode of preserving HTTP method names
092         */
093        public void setIncludeHttpMethodName(IncludeHttpMethodName includeHttpMethodName) {
094                this.includeHttpMethodName = includeHttpMethodName;
095        }
096
097        /**
098         * Returns Simon name for the specified HTTP request with the specified prefix. By default it contains URI without parameters with
099         * all slashes replaced for dots (slashes then determines position in Simon hierarchy). Method can NOT be overridden, but some of the
100         * following steps can:
101         * <ol>
102         * <li>the request is transformed to the string ({@link #requestToStringForMonitorName(javax.servlet.http.HttpServletRequest)}, can be overridden),</li>
103         * <li>the characters that are not allowed as part of the Simon name are replaced with underscore (_) - replacement regex can be changed with {@link #setReplaceUnallowed(String)},</li>
104         * <li>any subsequent slashes and dots are replaced with a single dot ({@link org.javasimon.Manager#HIERARCHY_DELIMITER})</li>
105         * </ol>
106         *
107         * @param request HTTP request
108         * @return fully qualified name of the Simon
109         * @see #requestToStringForMonitorName(javax.servlet.http.HttpServletRequest)
110         */
111        protected String getMonitorName(HttpServletRequest request) {
112                String uri = requestToStringForMonitorName(request);
113                String localName = SimonServletFilterUtils.getSimonName(uri, unallowedCharacterReplacer);
114                String monitorName;
115                if (prefix == null || prefix.isEmpty()) {
116                        monitorName = localName;
117                } else {
118                        monitorName = prefix + Manager.HIERARCHY_DELIMITER + localName;
119                }
120
121                if (includeMethodName(request)) {
122                        monitorName += Manager.HIERARCHY_DELIMITER + request.getMethod();
123                }
124
125                return monitorName;
126        }
127
128        private boolean includeMethodName(HttpServletRequest request) {
129                return includeHttpMethodName == IncludeHttpMethodName.ALWAYS ||
130                                (includeHttpMethodName == IncludeHttpMethodName.NON_GET && !request.getMethod().equals(GET_METHOD));
131        }
132
133        /**
134         * Performs the first step in getting the monitor name from the specified HTTP request - here any custom ignore logic should happen.
135         * By default the name is URI (without parameters - see {@link javax.servlet.http.HttpServletRequest#getRequestURI()}) with JSessionID
136         * removed (see {@link #removeJSessionIdFromUri(String)}) and any trailing stuff removed (see {@link #removeTrailingStuff(String)}).
137         * This method can be overridden for two typical reasons:
138         * <ul>
139         * <li>Name of the monitor (Stopwatch) should be based on something else then URI,</li>
140         * <li>there are other parts of the name that should be modified or ignored (e.g., REST parameters that are part of the URI).</li>
141         * </ul>
142         *
143         * @param request HTTP request
144         * @return preprocessed URI that will be converted to the Simon name
145         * @see #getMonitorName(javax.servlet.http.HttpServletRequest)
146         * @see #removeJSessionIdFromUri(String)
147         */
148        protected String requestToStringForMonitorName(HttpServletRequest request) {
149                String uri = request.getRequestURI();
150                uri = removeJSessionIdFromUri(uri);
151                uri = removeTrailingStuff(uri);
152                return uri;
153        }
154
155        /**
156         * Removes JSESSIONID parameter from URI. By default it is not necessary to handle parameters, as incoming URI already is without
157         * parameters, but JSESSIONID sometimes come before parameters in other forms and this method tries to remove such forms.
158         * <p/>
159         * Called by default implementation of {@link #requestToStringForMonitorName(javax.servlet.http.HttpServletRequest)} and extracted
160         * so it can be used by any overriding implementation of the same method. Method can be overridden if the default behavior is not
161         * sufficient.
162         *
163         * @param uri preprocessed URI that may contain JSessionID
164         * @return preprocessed URI without JSessionID
165         * @see #requestToStringForMonitorName(javax.servlet.http.HttpServletRequest)
166         */
167        protected String removeJSessionIdFromUri(String uri) {
168                return jsessionParameterReplacer.process(uri);
169        }
170
171        /**
172         * Removes any trailing slashes followed by other characters if none of them is alphabetic. This should take care of some REST
173         * parameters (numeric id-s) and it also removes trailing slashes to avoid empty local Simon names which is forbidden.
174         * <p/>
175         * Called by default implementation of {@link #requestToStringForMonitorName(javax.servlet.http.HttpServletRequest)} and extracted
176         * so it can be used by any overriding implementation of the same method. Method can be overridden if the default behavior is not
177         * sufficient.
178         *
179         * @param uri preprocessed URI that may contain JSessionID
180         * @return preprocessed URI without JSessionID
181         * @see #requestToStringForMonitorName(javax.servlet.http.HttpServletRequest)
182         */
183        protected String removeTrailingStuff(String uri) {
184                return trailingStuffReplacer.process(uri);
185        }
186
187        /**
188         * Indicates whether the HTTP Request should be monitored - method is intended for override.
189         * Default behavior ignores URIs ending with .css, .png, .gif, .jpg and .js (ignores casing).
190         *
191         * @param httpServletRequest HTTP Request
192         * @return true to enable request monitoring, false either
193         */
194        @Override
195        public boolean isMonitored(HttpServletRequest httpServletRequest) {
196                String uri = httpServletRequest.getRequestURI().toLowerCase();
197                return !(uri.endsWith(".css") || uri.endsWith(".png") || uri.endsWith(".gif") || uri.endsWith(".jpg") || uri.endsWith(".js"));
198        }
199
200        /**
201         * Get a stopwatch for given HTTP request.
202         *
203         * @param request Method HTTP request
204         * @return Stopwatch for the HTTP request
205         */
206        @Override
207        public Stopwatch getMonitor(HttpServletRequest request) {
208                final Stopwatch stopwatch = super.getMonitor(request);
209                if (stopwatch.getNote() == null) {
210                        stopwatch.setNote(request.getRequestURI());
211                }
212                return stopwatch;
213        }
214
215        /**
216         * Wraps given stop watch source in a cache.
217         *
218         * @param stopwatchSource Stopwatch source
219         * @return Cached stopwatch source
220         */
221        public static StopwatchSource<HttpServletRequest> newCacheStopwatchSource(StopwatchSource<HttpServletRequest> stopwatchSource) {
222                return new CachedStopwatchSource<HttpServletRequest, String>(stopwatchSource) {
223                        @Override
224                        protected String getLocationKey(HttpServletRequest location) {
225                                return location.getRequestURI();
226                        }
227                };
228        }
229}