org.echocat.jemoni.jmx.support.ServletHealth.java Source code

Java tutorial

Introduction

Here is the source code for org.echocat.jemoni.jmx.support.ServletHealth.java

Source

/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat JeMoni, Copyright (c) 2012-2013 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jemoni.jmx.support;

import org.apache.commons.collections15.map.LRUMap;
import org.echocat.jemoni.jmx.JmxRegistry;
import org.echocat.jemoni.jmx.Registration;
import org.echocat.jemoni.jmx.support.ServletHealth.ScopeMapping;
import org.echocat.jomon.runtime.concurrent.StopWatch;
import org.echocat.jomon.runtime.iterators.ConvertingIterator;
import org.echocat.jomon.runtime.math.OverPeriodAverageDoubleCounter;
import org.echocat.jomon.runtime.math.OverPeriodCounter;
import org.echocat.jomon.runtime.util.Duration;
import org.echocat.jomon.runtime.util.Entry;
import org.echocat.jomon.runtime.util.Entry.Impl;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.management.*;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import static java.lang.Thread.currentThread;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableMap;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.echocat.jemoni.jmx.JmxRegistry.getLocalInstance;
import static org.echocat.jemoni.jmx.support.SpringUtils.getBeanFor;
import static org.echocat.jomon.runtime.util.ResourceUtils.closeQuietly;

public class ServletHealth implements AutoCloseable, Filter, Iterable<Entry<Pattern, ScopeMapping>> {

    public static final String MAPPING_INIT_ATTRIBUTE = "mapping";
    public static final String REGISTRY_REF_INIT_ATTRIBUTE = "registry-ref";

    public static final String EXCLUDES_HOSTS = "excludeHosts";
    public static final String INCLUDES_HOSTS = "includeHosts";

    public static final String INTERCEPTOR_INIT_ATTRIBUTE = "interceptor";
    public static final String INTERCEPTOR_REF_INIT_ATTRIBUTE = INTERCEPTOR_INIT_ATTRIBUTE + "-ref";

    public static final String REQUESTS_PER_SECOND_ATTRIBUTE_NAME = "requestsPerSecond";
    public static final String AVERAGE_REQUEST_DURATION_ATTRIBUTE_NAME = "averageRequestDuration";
    public static final String CURRENT_REQUEST_STOP_WATCH_ATTRIBUTE_NAME = ServletHealth.class.getName()
            + ".currentRequestStopWatch";

    private final Map<String, ScopeMapping> _pathToMappingCache = new LRUMap<>(10000);

    private JmxRegistry _registry;

    private Map<Pattern, ScopeMapping> _patternToMapping;
    private Map<String, ScopeMapping> _nameToMapping;
    private ServletHealthInterceptor _interceptor;

    private Registration _registration;

    public ServletHealth() {
        setMapping(null);
    }

    public void setMapping(@Nullable String mappingAsString) {
        final Map<Pattern, ScopeMapping> patternToMapping = new LinkedHashMap<>();
        final ServletHealthInterceptor interceptor = _interceptor;

        if (mappingAsString != null) {
            for (String parts : mappingAsString.split("[,\\n\\r]")) {
                final String trimmedPart = parts.trim();
                if (!trimmedPart.isEmpty()) {
                    final int lastArrow = trimmedPart.lastIndexOf('>');
                    if (lastArrow > 0 && lastArrow + 1 < trimmedPart.length()) {
                        try {
                            final Pattern pattern = Pattern.compile(trimmedPart.substring(0, lastArrow).trim());
                            final String name = trimmedPart.substring(lastArrow + 1).trim();
                            if (name.isEmpty()) {
                                throw new IllegalArgumentException("Illegal formatted mapping: " + mappingAsString);
                            }
                            final Collection<String> possibleSpecificNames = interceptor != null
                                    ? interceptor.getPossibleNames(name)
                                    : null;
                            patternToMapping.put(pattern, new ScopeMapping(name, possibleSpecificNames));
                        } catch (PatternSyntaxException e) {
                            throw new IllegalArgumentException("Illegal formatted mapping: " + mappingAsString, e);
                        }
                    } else {
                        throw new IllegalArgumentException("Illegal formatted mapping: " + mappingAsString);
                    }
                }
            }
        }

        patternToMapping.put(null, new ScopeMapping(null, Collections.<String>emptyList()));

        _nameToMapping = asNameToMapping(patternToMapping.values());
        _patternToMapping = unmodifiableMap(patternToMapping);
        synchronized (_pathToMappingCache) {
            _pathToMappingCache.clear();
        }
    }

    @Nullable
    public String getMapping() {
        final StringBuilder sb = new StringBuilder();

        for (Map.Entry<Pattern, ScopeMapping> patternAndMapping : _patternToMapping.entrySet()) {
            if (patternAndMapping.getKey() != null) {
                if (sb.length() > 0) {
                    sb.append(",\n");
                }
                sb.append(patternAndMapping.getKey()).append('>')
                        .append(patternAndMapping.getValue().getDefaultName());
            }
        }

        return sb.length() > 0 ? sb.toString() : null;
    }

    public ServletHealthInterceptor getInterceptor() {
        return _interceptor;
    }

    public void setInterceptor(ServletHealthInterceptor interceptor) {
        _interceptor = interceptor;
        setMapping(getMapping());
    }

    public void setRegistry(@Nullable JmxRegistry registry) {
        _registry = registry;
    }

    @Nonnull
    public JmxRegistry getRegistry() {
        final JmxRegistry registry = _registry;
        return registry != null ? registry : getLocalInstance();
    }

    @Nonnull
    protected Map<String, ScopeMapping> asNameToMapping(@Nonnull Iterable<ScopeMapping> values) {
        final Map<String, ScopeMapping> result = new LinkedHashMap<>();
        for (ScopeMapping mapping : values) {
            for (String name : mapping.getAllNames()) {
                result.put(name, mapping);
            }
        }
        return unmodifiableMap(result);
    }

    public void init() {
        _registration = getRegistry().register(new MBeanInformation(), getClass());
    }

    @Override
    public void init(@Nonnull FilterConfig filterConfig) throws ServletException {
        final String mapping = filterConfig.getInitParameter(MAPPING_INIT_ATTRIBUTE);
        if (mapping != null) {
            setMapping(mapping);
        }
        final String registryRef = filterConfig.getInitParameter(REGISTRY_REF_INIT_ATTRIBUTE);
        if (!isEmpty(registryRef)) {
            setRegistry(getBeanFor(filterConfig.getServletContext(), registryRef, JmxRegistry.class));
        }
        final String interceptor = filterConfig.getInitParameter(INTERCEPTOR_INIT_ATTRIBUTE);
        if (!isEmpty(interceptor)) {
            setInterceptor(loadInterceptor(interceptor));
        }
        final String interceptorRef = filterConfig.getInitParameter(INTERCEPTOR_REF_INIT_ATTRIBUTE);
        if (!isEmpty(interceptorRef)) {
            setInterceptor(
                    getBeanFor(filterConfig.getServletContext(), interceptorRef, ServletHealthInterceptor.class));
        }
        handleHostIncludeExcludesIfNeeded(filterConfig);
        init();
    }

    protected void handleHostIncludeExcludesIfNeeded(@Nonnull FilterConfig filterConfig) {
        final String includeHosts = filterConfig.getInitParameter(INCLUDES_HOSTS);
        final String excludeHosts = filterConfig.getInitParameter(EXCLUDES_HOSTS);
        if (!isEmpty(includeHosts) || !isEmpty(excludeHosts)) {
            final ServletHealthInterceptor originalInterceptor = getInterceptor();
            final AllowedHostsServletHealthInterceptor newInterceptor = new AllowedHostsServletHealthInterceptor();
            newInterceptor.setIncludesPattern(includeHosts);
            newInterceptor.setExcludesPattern(excludeHosts);
            setInterceptor(originalInterceptor != null
                    ? new CombinedServletHealthInterceptor(originalInterceptor, newInterceptor)
                    : newInterceptor);
        }
    }

    @Nonnull
    public ServletHealthInterceptor loadInterceptor(@Nonnull String interceptorTypeName) throws ServletException {
        final Class<?> interceptorType;
        try {
            interceptorType = currentThread().getContextClassLoader().loadClass(interceptorTypeName);
        } catch (ClassNotFoundException e) {
            throw new ServletException("Could not find interceptor of type " + interceptorTypeName + ".", e);
        }
        if (!ServletHealthInterceptor.class.isAssignableFrom(interceptorType)) {
            throw new ServletException("Defined interceptor type " + interceptorTypeName + " does not implements "
                    + ServletHealthInterceptor.class.getName() + ".");
        }
        try {
            return (ServletHealthInterceptor) interceptorType.newInstance();
        } catch (Exception e) {
            throw new ServletException(
                    "Could not create an instance of interceptor " + interceptorType.getName() + ".", e);
        }
    }

    @Override
    public void close() {
        try {
            closeQuietly(_registration);
        } finally {
            _registration = null;
        }
    }

    @Override
    public void destroy() {
        close();
    }

    @Override
    @Nonnull
    public Iterator<Entry<Pattern, ScopeMapping>> iterator() {
        return new ConvertingIterator<Map.Entry<Pattern, ScopeMapping>, Entry<Pattern, ScopeMapping>>(
                _patternToMapping.entrySet().iterator()) {
            @Override
            protected Entry<Pattern, ScopeMapping> convert(Map.Entry<Pattern, ScopeMapping> input) {
                return new Impl<>(input.getKey(), input.getValue());
            }
        };
    }

    @Override
    public void doFilter(@Nonnull ServletRequest request, @Nonnull ServletResponse response,
            @Nonnull FilterChain chain) throws IOException, ServletException {
        final StopWatch stopWatch = new StopWatch();
        request.setAttribute(CURRENT_REQUEST_STOP_WATCH_ATTRIBUTE_NAME, stopWatch);
        final ScopeMapping globalMapping = _patternToMapping.get(null);
        final ScopeMapping specificMapping = request instanceof HttpServletRequest
                ? getMappingFor(((HttpServletRequest) request).getRequestURI())
                : null;
        try {
            chain.doFilter(request, response);
        } finally {
            request.removeAttribute(CURRENT_REQUEST_STOP_WATCH_ATTRIBUTE_NAME);
            final Duration duration = stopWatch.getCurrentDuration();
            final ServletHealthInterceptor interceptor = _interceptor;
            if (interceptor == null || interceptor.isRecordAllowed(request, globalMapping, specificMapping)) {
                globalMapping.record(null, duration);
                if (specificMapping != null) {
                    final String targetName = interceptor != null
                            ? interceptor.getSpecificTargetName(request, specificMapping)
                            : null;
                    specificMapping.record(targetName, duration);
                }
            }
        }
    }

    @Nullable
    public static Duration findCurrentRequestDurationOf(@Nonnull ServletRequest request) {
        final Object plainStopWatch = request.getAttribute(CURRENT_REQUEST_STOP_WATCH_ATTRIBUTE_NAME);
        return plainStopWatch instanceof StopWatch ? ((StopWatch) plainStopWatch).getCurrentDuration() : null;
    }

    @Nullable
    protected ScopeMapping getMappingFor(@Nonnull String path) {
        ScopeMapping mapping;
        synchronized (_pathToMappingCache) {
            mapping = _pathToMappingCache.get(path);
        }
        if (mapping == null) {
            for (Map.Entry<Pattern, ScopeMapping> patternAndScope : _patternToMapping.entrySet()) {
                final Pattern pattern = patternAndScope.getKey();
                if (pattern != null && pattern.matcher(path).matches()) {
                    mapping = patternAndScope.getValue();
                    break;
                }
            }
            synchronized (_pathToMappingCache) {
                _pathToMappingCache.put(path, mapping);
            }
        }
        return mapping;
    }

    @Nullable
    protected ScopeMapping getMapping(@Nonnull String defaultName) {
        return _nameToMapping.get(defaultName);
    }

    public static class ScopeMapping {

        private final Map<String, MeasurePoints> _nameToMeasurePoints;
        private final String _defaultName;

        public ScopeMapping(@Nullable String defaultName, @Nullable Collection<String> names) {
            _defaultName = defaultName;
            _nameToMeasurePoints = new LinkedHashMap<>();
            if (names == null || names.isEmpty()) {
                _nameToMeasurePoints.put(defaultName, new MeasurePoints());
            } else {
                for (String possibleSpecificName : names) {
                    _nameToMeasurePoints.put(possibleSpecificName, new MeasurePoints());
                }
            }
        }

        @Nullable
        public String getDefaultName() {
            return _defaultName;
        }

        @Nonnull
        public Collection<String> getAllNames() {
            return unmodifiableCollection(_nameToMeasurePoints.keySet());
        }

        @Nonnegative
        public double getAverageRequestDuration(@Nonnull String name) {
            return _nameToMeasurePoints.get(name).getAverageRequestDuration();
        }

        @Nonnegative
        public double getRequestsPerSecond(@Nonnull String name) {
            return _nameToMeasurePoints.get(name).getRequestsPerSecond();
        }

        public void record(@Nullable String targetName, @Nonnull Duration requestDuration) {
            final String name = targetName != null ? targetName : _defaultName;
            final MeasurePoints measurePoints = _nameToMeasurePoints.get(name);
            if (measurePoints == null) {
                throw new IllegalStateException("Used a name that is unknown: " + name);
            }
            measurePoints.record(requestDuration);
        }

    }

    public static class MeasurePoints {
        private final OverPeriodCounter _requestsPerSecond = new OverPeriodCounter(new Duration("1m"),
                new Duration("1s"));
        private final OverPeriodAverageDoubleCounter _averageRequestDuration = new OverPeriodAverageDoubleCounter(
                new Duration("1m"), new Duration("1s"));

        public void record(@Nonnull Duration requestDuration) {
            _requestsPerSecond.record();
            _averageRequestDuration.record((double) requestDuration.toMilliSeconds());
        }

        @Nonnegative
        public double getAverageRequestDuration() {
            return _averageRequestDuration.get();
        }

        @Nonnegative
        public double getRequestsPerSecond() {
            return _requestsPerSecond.getAsDouble();
        }

    }

    protected class MBeanInformation implements DynamicMBean {

        @Override
        public Object getAttribute(String name)
                throws AttributeNotFoundException, MBeanException, ReflectionException {
            final Object result;
            final int firstDot = name.indexOf('.');
            final boolean withPrefix = firstDot > 0 && firstDot + 1 < name.length();
            final String mappingName = withPrefix ? name.substring(0, firstDot) : null;
            final String valueName = withPrefix ? name.substring(firstDot + 1) : name;
            final ScopeMapping mapping = _nameToMapping.get(mappingName);
            if (mapping != null) {
                if (REQUESTS_PER_SECOND_ATTRIBUTE_NAME.equals(valueName)) {
                    result = mapping.getRequestsPerSecond(mappingName);
                } else if (AVERAGE_REQUEST_DURATION_ATTRIBUTE_NAME.equals(valueName)) {
                    result = mapping.getAverageRequestDuration(mappingName);
                } else {
                    throw new AttributeNotFoundException();
                }
            } else {
                throw new AttributeNotFoundException();
            }
            return result;
        }

        @Nonnull
        protected MBeanAttributeInfo[] getMBeanAttributesFor() {
            final List<MBeanAttributeInfo> attributes = new ArrayList<>();
            for (ScopeMapping mapping : _patternToMapping.values()) {
                for (String name : mapping.getAllNames()) {
                    attributes.add(new MBeanAttributeInfo(
                            name != null ? name + "." + REQUESTS_PER_SECOND_ATTRIBUTE_NAME
                                    : REQUESTS_PER_SECOND_ATTRIBUTE_NAME,
                            Double.class.getName(), null, true, false, false));
                    attributes.add(new MBeanAttributeInfo(
                            name != null ? name + "." + AVERAGE_REQUEST_DURATION_ATTRIBUTE_NAME
                                    : AVERAGE_REQUEST_DURATION_ATTRIBUTE_NAME,
                            Double.class.getName(), null, true, false, false));
                }
            }
            return attributes.toArray(new MBeanAttributeInfo[attributes.size()]);
        }

        @Override
        public MBeanInfo getMBeanInfo() {
            return new MBeanInfo(ServletHealth.this.getClass().getName(),
                    "Display basic information of servlet handling.", getMBeanAttributesFor(), null, null, null);
        }

        @Override
        public Object invoke(String actionName, Object[] params, String[] signature)
                throws MBeanException, ReflectionException {
            throw new UnsupportedOperationException();
        }

        @Override
        public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
                InvalidAttributeValueException, MBeanException, ReflectionException {
            throw new AttributeNotFoundException();
        }

        @Override
        public AttributeList getAttributes(String[] attributes) {
            throw new UnsupportedOperationException();
        }

        @Override
        public AttributeList setAttributes(AttributeList attributes) {
            throw new UnsupportedOperationException();
        }

    }

}