001    package org.javasimon.javaee;
002    
003    import javax.servlet.http.HttpServletRequest;
004    
005    import org.javasimon.Manager;
006    import org.javasimon.Stopwatch;
007    import org.javasimon.source.AbstractStopwatchSource;
008    import org.javasimon.source.CachedStopwatchSource;
009    import org.javasimon.source.StopwatchSource;
010    import 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     */
024    public 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    }