com.adobe.acs.tools.explain_query.impl.ExplainQueryServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.adobe.acs.tools.explain_query.impl.ExplainQueryServlet.java

Source

/*
 * #%L
 * ACS AEM Tools Bundle
 * %%
 * Copyright (C) 2014 Adobe
 * %%
 * 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.
 * #L%
 */
package com.adobe.acs.tools.explain_query.impl;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.spi.FilterReply;
import com.adobe.acs.commons.util.OsgiPropertyUtil;
import com.day.cq.search.PredicateGroup;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.SearchResult;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyUnbounded;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.jackrabbit.api.jmx.QueryStatManagerMBean;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.slf4j.Marker;

import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;
import javax.management.openmbean.CompositeData;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SlingServlet(label = "ACS AEM Tools - Explain Query Servlet", description = "End-point for Apache Jackrabbit Oak query explanations.", methods = {
        "GET", "POST" }, resourceTypes = { "acs-tools/components/explain-query" }, selectors = {
                "explain" }, extensions = { "json" }, metatype = true)
public class ExplainQueryServlet extends SlingAllMethodsServlet {
    private static final Logger log = LoggerFactory.getLogger(ExplainQueryServlet.class);

    //MDC key defined in org.apache.jackrabbit.oak.query.QueryEngineImpl
    private static final String OAK_QUERY_ANALYZE = "oak.query.analyze";

    private static final String SQL = "sql";

    private static final String SQL2 = "JCR-SQL2";

    private static final String XPATH = "xpath";

    private static final String QUERY_BUILDER = "queryBuilder";

    private static final String[] LANGUAGES = new String[] { SQL, SQL2, XPATH };

    private static final Pattern PROPERTY_INDEX_PATTERN = Pattern.compile("\\/\\*\\sproperty\\s([^\\s=]+)[=\\s]");

    private static final Pattern FILTER_PATTERN = Pattern
            .compile("\\[[^\\s]+\\]\\sas\\s\\[[^\\s]+\\]\\s\\/\\*\\sFilter\\(");

    @Property(label = "Query Logger Names", description = "Logger names from which logs need to be collected while a query is executed. "
            + "Provide in the format '<package-name>=<mdc-filter-name>' where <mdc-filter-name>' is optional.", unbounded = PropertyUnbounded.ARRAY, value = {
                    "org.apache.jackrabbit.oak.query=" + OAK_QUERY_ANALYZE,
                    "org.apache.jackrabbit.oak.plugins.index=" + OAK_QUERY_ANALYZE,
                    "com.day.cq.search.impl.builder.QueryImpl" })
    private static final String PROP_LOGGER_NAMES = "log.logger-names";

    private static final String DEFAULT_PATTERN = "%msg%n";

    @Property(label = "Log Pattern", description = "Message Pattern for formatting the log messages. [ Default: "
            + DEFAULT_PATTERN + " ]", value = DEFAULT_PATTERN)
    private static final String PROP_MSG_PATTERN = "log.pattern";

    private static final int DEFAULT_LIMIT = 100;

    @Property(label = "Log Limit", description = "Number of log message which should be collected in memory", intValue = DEFAULT_LIMIT)
    private static final String PROP_LOG_COUNT_LIMIT = "log.message-count-limit";

    @Reference
    private QueryStatManagerMBean queryStatManagerMBean;

    @Reference
    private QueryBuilder queryBuilder;

    private QueryLogCollector logCollector = null;

    private final AtomicReference<ServiceRegistration> logCollectorReg = new AtomicReference<ServiceRegistration>();

    private final AtomicInteger logCollectorRegCount = new AtomicInteger();

    private BundleContext bundleContext;
    private Map<String, String> loggers = new HashMap<String, String>();
    private String pattern = DEFAULT_PATTERN;
    private int msgCountLimit = DEFAULT_LIMIT;

    @SuppressWarnings("unchecked")
    @Override
    protected final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws ServletException, IOException {
        final JSONObject json = new JSONObject();

        try {
            json.put("slowQueries", this.compositeQueryDataToJSON(
                    (Collection<CompositeData>) queryStatManagerMBean.getSlowQueries().values()));
        } catch (JSONException e) {
            log.error("Unable to serial Slow Queries into JSON: {}", e.getMessage());
        }

        try {
            json.put("popularQueries", this.compositeQueryDataToJSON(
                    (Collection<CompositeData>) queryStatManagerMBean.getPopularQueries().values()));
        } catch (JSONException e) {
            log.error("Unable to serial Popular Queries into JSON: {}", e.getMessage());
        }

        response.setContentType("application/json");
        response.getWriter().print(json.toString());
    }

    @Override
    protected final void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws ServletException, IOException {

        boolean logCollectorRegistered = false;
        final ResourceResolver resourceResolver = request.getResourceResolver();

        String statement = StringUtils.removeStartIgnoreCase(request.getParameter("statement"), "EXPLAIN ");
        String language = request.getParameter("language");

        final Session session = resourceResolver.adaptTo(Session.class);

        try {
            // Mark this thread as an Explain Query thread for TurboFiltering
            registerLogCollector();
            logCollectorRegistered = true;

            final JSONObject json = new JSONObject();
            json.put("statement", statement);
            json.put("language", language);

            json.put("explain", explainQuery(session, statement, language));

            boolean collectExecutionTime = "true"
                    .equals(StringUtils.defaultIfEmpty(request.getParameter("executionTime"), "false"));

            boolean collectCount = "true"
                    .equals(StringUtils.defaultIfEmpty(request.getParameter("resultCount"), "false"));

            if (collectExecutionTime) {
                json.put("heuristics", this.getHeuristics(session, statement, language, collectCount));
            }

            response.setContentType("application/json");
            response.getWriter().print(json.toString());

        } catch (RepositoryException e) {
            log.error(e.getMessage());
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (JSONException e) {
            log.error(e.getMessage());
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (logCollectorRegistered) {
                unregisterLogCollector();
            }
        }
    }

    private JSONObject explainQuery(final Session session, final String statement, final String language)
            throws RepositoryException, JSONException {
        final QueryManager queryManager = session.getWorkspace().getQueryManager();
        final JSONObject json = new JSONObject();
        final String collectorKey = startCollection();
        final QueryResult queryResult;
        final String effectiveLanguage;
        final String effectiveStatement;

        if (language.equals(QUERY_BUILDER)) {
            effectiveLanguage = XPATH;
            final String[] lines = StringUtils.split(statement, '\n');
            final Map<String, String> params = OsgiPropertyUtil.toMap(lines, "=", false, null, true);

            final com.day.cq.search.Query query = queryBuilder.createQuery(PredicateGroup.create(params), session);
            effectiveStatement = query.getResult().getQueryStatement();
        } else {
            effectiveStatement = statement;
            effectiveLanguage = language;
        }
        try {
            final Query query = queryManager.createQuery("explain " + effectiveStatement, effectiveLanguage);
            queryResult = query.execute();
        } finally {
            synchronized (this.logCollector) {
                if (this.logCollector != null) {
                    List<String> logs = this.logCollector.getLogs(collectorKey);
                    json.put("logs", this.logCollector.getLogs(collectorKey));

                    if (logs.size() == this.logCollector.msgCountLimit) {
                        json.put("logsTruncated", true);
                    }
                }
                stopCollection(collectorKey);
            }
        }

        final RowIterator rows = queryResult.getRows();
        final Row firstRow = rows.nextRow();

        final String plan = firstRow.getValue("plan").getString();
        json.put("plan", plan);

        final JSONArray propertyIndexes = new JSONArray();

        final Matcher propertyMatcher = PROPERTY_INDEX_PATTERN.matcher(plan);
        /* Property Index */
        while (propertyMatcher.find()) {
            final String match = propertyMatcher.group(1);
            if (StringUtils.isNotBlank(match)) {
                propertyIndexes.put(StringUtils.stripToEmpty(match));
            }
        }

        if (propertyIndexes.length() > 0) {
            json.put("propertyIndexes", propertyIndexes);
        }

        final Matcher filterMatcher = FILTER_PATTERN.matcher(plan);
        if (filterMatcher.find()) {
            /* Filter (nodeType index) */

            propertyIndexes.put("nodeType");
            json.put("propertyIndexes", propertyIndexes);
            json.put("slow", true);
        }

        if (StringUtils.contains(plan, " /* traverse ")) {
            /* Traversal */
            json.put("traversal", true);
            json.put("slow", true);
        }

        if (StringUtils.contains(plan, " /* aggregate ")) {
            /* Aggregate - Fulltext */
            json.put("aggregate", true);
        }

        return json;
    }

    private JSONObject getHeuristics(final Session session, final String statement, final String language,
            final boolean getCount) throws RepositoryException, JSONException {

        int count = 0;

        final QueryManager queryManager = session.getWorkspace().getQueryManager();
        final JSONObject json = new JSONObject();

        long executionTime;
        long getNodesTime;
        long countTime = 0L;

        if (language.equals(QUERY_BUILDER)) {
            final String[] lines = StringUtils.split(statement, '\n');
            final Map<String, String> params = OsgiPropertyUtil.toMap(lines, "=", false, null, true);

            final com.day.cq.search.Query query = queryBuilder.createQuery(PredicateGroup.create(params), session);
            long start = System.currentTimeMillis();
            SearchResult result = query.getResult();
            executionTime = System.currentTimeMillis() - start;

            start = System.currentTimeMillis();
            result.getNodes();
            getNodesTime = System.currentTimeMillis() - start;

            if (getCount) {
                count = result.getHits().size();
                countTime = System.currentTimeMillis() - start;
            }

        } else {
            final Query query = queryManager.createQuery(statement, language);

            long start = System.currentTimeMillis();
            final QueryResult queryResult = query.execute();
            executionTime = System.currentTimeMillis() - start;

            start = System.currentTimeMillis();
            queryResult.getNodes();
            getNodesTime = System.currentTimeMillis() - start;

            if (getCount) {
                final NodeIterator nodes = queryResult.getNodes();

                while (nodes.hasNext()) {
                    nodes.next();
                    count++;
                }

                countTime = System.currentTimeMillis() - start;
            }
        }

        json.put("executeTime", executionTime);
        json.put("getNodesTime", getNodesTime);

        if (getCount) {
            json.put("count", count);
            json.put("countTime", countTime);
        }

        json.put("totalTime", executionTime + getNodesTime + countTime);

        return json;
    }

    private JSONArray compositeQueryDataToJSON(Collection<CompositeData> queries) throws JSONException {
        final JSONArray jsonArray = new JSONArray();

        for (CompositeData query : queries) {
            Long duration = (Long) query.get("duration");
            Integer occurrenceCount = (Integer) query.get("occurrenceCount");
            String language = (String) query.get("language");
            String statement = (String) query.get("statement");

            if (!ArrayUtils.contains(LANGUAGES, language)) {
                // Not a supported language
                continue;
            } else if (StringUtils.startsWithIgnoreCase(statement, "EXPLAIN ")
                    || StringUtils.startsWithIgnoreCase(statement, "MEASURE ")) {
                // Don't show EXPLAIN or MEASURE queries
                continue;
            }

            final JSONObject json = new JSONObject();

            try {
                json.put("duration", duration);
                json.put("language", language);
                json.put("occurrenceCount", occurrenceCount);
                json.put("statement", statement);

                jsonArray.put(json);
            } catch (JSONException e) {
                log.warn("Could not add query to results [ {} ]", statement);
                continue;
            }
        }

        return jsonArray;
    }

    private String startCollection() {
        final String collectorKey = UUID.randomUUID().toString();
        MDC.put(QueryLogCollector.COLLECTOR_KEY, collectorKey);
        return collectorKey;
    }

    private void stopCollection(String key) {
        synchronized (this.logCollector) {
            MDC.remove(key);
            if (this.logCollector != null) {
                this.logCollector.stopCollection(key);
            }
        }
    }

    private static boolean checkMDCSupport(BundleContext context) {
        //MDC support is present since 1.0.9                     s
        Version versionWithMDCSupport = new Version(1, 0, 9);
        for (Bundle b : context.getBundles()) {
            if ("org.apache.jackrabbit.oak-core".equals(b.getSymbolicName())) {
                return versionWithMDCSupport.compareTo(b.getVersion()) <= 0;
            }
        }

        //By default it is assumed that MDC support is present
        return true;
    }

    /**
     * TurboFilters causes slowness as they are executed on critical path
     * Hence care is taken to only register the filter only when required
     * Logic below ensures that filter is only registered for the duration
     * or request which needs to be "monitored".
     *
     * If multiple such request are performed then also only one filter gets
     * registered
     */
    private void registerLogCollector() {
        synchronized (logCollectorRegCount) {
            int count = logCollectorRegCount.getAndIncrement();
            if (count == 0) {
                ServiceRegistration reg = bundleContext.registerService(TurboFilter.class.getName(),
                        this.logCollector, null);
                logCollectorReg.set(reg);
            }
        }
    }

    private void unregisterLogCollector() {
        synchronized (logCollectorRegCount) {
            int count = logCollectorRegCount.decrementAndGet();
            if (count == 0) {
                ServiceRegistration reg = logCollectorReg.getAndSet(null);
                reg.unregister();
            }
        }
    }

    @Activate
    private void activate(Map<String, ?> config, BundleContext context) {
        this.bundleContext = context;

        this.loggers = OsgiPropertyUtil
                .toMap(PropertiesUtil.toStringArray(config.get(PROP_LOGGER_NAMES), new String[0]), "=", true, null);

        if (loggers != null && !loggers.isEmpty()) {
            this.pattern = PropertiesUtil.toString(config.get(PROP_MSG_PATTERN), DEFAULT_PATTERN);
            this.msgCountLimit = PropertiesUtil.toInteger(config.get(PROP_LOG_COUNT_LIMIT), DEFAULT_LIMIT);

            // Create a single logCollector; this will be used as TurboFilters are register with the execution of
            // this Servlet
            this.logCollector = new QueryLogCollector(loggers, pattern, msgCountLimit, checkMDCSupport(context));
        } else {
            this.logCollector = null;
        }
    }

    @Deactivate
    private void deactivate() {
        ServiceRegistration reg = logCollectorReg.getAndSet(null);

        if (reg != null) {
            reg.unregister();
        }
    }

    private static final class QueryLogCollector extends TurboFilter {
        private static final String COLLECTOR_KEY = "collectorKey";

        private final Map<String, String> loggers;

        private final Layout<ILoggingEvent> layout;

        private final int msgCountLimit;

        private final Map<String, List<ILoggingEvent>> logEvents = new ConcurrentHashMap<String, List<ILoggingEvent>>();

        private final boolean mdcEnabled;

        private QueryLogCollector(Map<String, String> loggers, String pattern, int msgCountLimit,
                boolean mdcEnabled) {
            this.loggers = loggers;
            this.msgCountLimit = msgCountLimit;
            this.layout = createLayout(pattern);
            this.mdcEnabled = mdcEnabled;

            if (!mdcEnabled) {
                log.debug("Current Oak version does not provide MDC. Explain log would have some extra entries");
            }
        }

        @Override
        public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format,
                Object[] params, Throwable t) {

            /** NO LOGGING IN THIS METHOD **/

            // If request is NOT an Explain Query generated thread, then always reply NEUTRAL
            // This check is extremely fast and incur almost no overhead.

            String collectorKey = MDC.get(COLLECTOR_KEY);
            if (collectorKey == null) {
                return FilterReply.NEUTRAL;
            }

            if (!acceptLogStatement(logger.getName())) {
                return FilterReply.NEUTRAL;
            }

            //isXXXEnabled call. Accept the call to allow actual message to be logged
            if (format == null) {
                return FilterReply.ACCEPT;
            }

            ILoggingEvent logEvent = new LoggingEvent(ch.qos.logback.classic.Logger.FQCN, logger, level, format, t,
                    params);
            log(collectorKey, logEvent);

            // Return NEUTRAL to allow normal logging of message depending on level
            return FilterReply.NEUTRAL;
        }

        public List<String> getLogs(String collectorKey) {
            List<ILoggingEvent> eventList = logEvents.get(collectorKey);
            if (eventList == null) {
                return Collections.emptyList();
            }
            List<String> result = new ArrayList<String>(eventList.size());
            for (ILoggingEvent e : eventList) {
                result.add(layout.doLayout(e));
            }
            return result;
        }

        public void stopCollection(String key) {
            logEvents.remove(key);
        }

        private void log(String collectorKey, ILoggingEvent e) {
            List<ILoggingEvent> eventList = logEvents.get(collectorKey);
            if (eventList == null) {
                eventList = new ArrayList<ILoggingEvent>();
                logEvents.put(collectorKey, eventList);
            }

            if (eventList.size() < msgCountLimit) {
                eventList.add(e);
            }
        }

        private boolean acceptLogStatement(String name) {
            for (final Map.Entry<String, String> entry : this.loggers.entrySet()) {
                if (name.startsWith(entry.getKey())) {
                    // log entry logger matches a configured logger

                    if (!mdcEnabled) {
                        // If MDC is not enabled, then matching logger name is good enough
                        return true;
                    } else if (mdcEnabled && entry.getValue() == null) {
                        // If MDC is enabled, but the logger is not configured to use MDC filtering then accept it
                        return true;
                    } else if (mdcEnabled && entry.getValue() != null && MDC.get(entry.getValue()) != null) {
                        // If MDC is enabled and a MDC filter value is specified, then the entry must have the
                        // MDC value
                        return true;
                    } else {
                        return false;
                    }
                }
            }

            return false;
        }

        private static Layout<ILoggingEvent> createLayout(String pattern) {
            PatternLayout pl = new PatternLayout();
            pl.setPattern(pattern);
            pl.setOutputPatternAsHeader(false);
            pl.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
            pl.start();
            return pl;
        }
    }
}