/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.sling.junit.impl.servlet; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.jacoco.agent.rt.IAgent; import org.osgi.service.component.ComponentContext; import org.osgi.service.http.HttpService; import org.osgi.service.http.NamespaceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.management.MBeanServer; import javax.management.MBeanServerInvocationHandler; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.management.ManagementFactory; import java.util.Dictionary; /** * This servlet exposes JaCoCo code coverage data over HTTP. See {@link #EXPLAIN} for usage information, * which is also available at /system/sling/jacoco after installing this servlet with the default settings. */ @SuppressWarnings("serial") @Component(immediate = true, metatype = true) public class JacocoServlet extends HttpServlet { private static final String PARAM_SESSION_ID = ":sessionId"; private static final String JMX_NAME = "org.jacoco:type=Runtime"; public static final String EXPLAIN = "This servlet exposes JaCoCo (http://www.eclemma.org/jacoco) code coverage data to HTTP clients by calling " + "JaCoCo's IAgent.getExecutionData(...).\n\n" + "POST requests reset the agent after returning the execution data, whereas GET " + "requests just return the data.\n" + "JaCoCo's session ID can be set via a " + PARAM_SESSION_ID + " request parameter.\n" + "The servlet returns 404 if the IAgent MBean is not available.\n\n" + "Please keep the JaCoCo security considerations in mind before enabling its agent: " + "JaCoCo's tcpserver and tcpclient modes and its JMX interface open ports that do " + "not require any authentication. See the JaCoCo documentation for details.\n\n" + "To activate JaCoCo on a Sling instance, start its JVM with the following option:\n\n" + "-javaagent:/path/to/jacocoagent.jar=dumponexit=false,jmx=true\n\n" + "The jacocoagent.jar file can be extracted from the appropriate maven artifact into the target directory " + "using 'mvn process-sources -P extractJacocoAgent' if you have this module's source code.\n\n" + "With this servlet installed, you can generate a JaCoCo coverage report " + "as follows (for example), from a folder that contains a pom.xml:\n\n" + " curl -o target/jacoco.exec http://localhost:8080/system/sling/jacoco/exec\n" + " mvn org.jacoco:jacoco-maven-plugin:report\n" + " open target/site/jacoco/index.html\n\n" ; private final Logger log = LoggerFactory.getLogger(getClass()); @Property(value="/system/sling/jacoco") static final String SERVLET_PATH_NAME = "servlet.path"; /** Requests ending with this subpath send the jacoco data */ public static final String EXEC_PATH = "/exec"; /** Non-null if we are registered with HttpService */ private String servletPath; @Reference private HttpService httpService; protected void activate(ComponentContext ctx) throws ServletException, NamespaceException { servletPath = getServletPath(ctx); if(servletPath == null) { log.info("Servlet path is null, not registering with HttpService"); } else { httpService.registerServlet(servletPath, this, null, null); log.info("Servlet registered at {}", servletPath); } } /** Return the path at which to mount this servlet, or null * if it must not be mounted. */ protected String getServletPath(ComponentContext ctx) { final Dictionary<?, ?> config = ctx.getProperties(); String result = (String)config.get(SERVLET_PATH_NAME); if(result != null && result.trim().length() == 0) { result = null; } return result; } protected void deactivate(ComponentContext ctx) throws ServletException, NamespaceException { if(servletPath != null) { httpService.unregister(servletPath); log.info("Servlet unregistered from path {}", servletPath); } servletPath = null; } /** * Get the jacoco execution data without resetting the agent * @param req the request * @param resp the response * @throws ServletException * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if(EXEC_PATH.equals(req.getPathInfo())) { final IAgent agent = getAgent(); if (agent == null) { final String msg = "The Jacoco agent MBean is not available\n\n"; resp.sendError(HttpServletResponse.SC_NOT_FOUND, msg + getUsageInfo()); } else { sendJacocoData(req, resp, false); resp.setContentType("application/octet-stream"); } } else { resp.setContentType("text/plain"); resp.setCharacterEncoding("UTF-8"); resp.getWriter().write(getUsageInfo()); resp.getWriter().flush(); } } /** * Get the jacoco execution data and reset the agent. Set the sessionId if :sessionId param exists. * @param req the request * @param resp the response * @throws ServletException * @throws IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { sendJacocoData(req, resp, true); } private void sendJacocoData(HttpServletRequest req, HttpServletResponse resp, boolean resetAgent) throws IOException { final IAgent agent = getAgent(); if (agent == null) { final String msg = "The Jacoco agent MBean is not available\n\n"; resp.sendError(HttpServletResponse.SC_NOT_FOUND, msg + getUsageInfo()); } else { resp.setContentType("application/octet-stream"); final String sessionId = req.getParameter(PARAM_SESSION_ID); log.info("Getting JaCoCo execution data, resetAgent={}", resetAgent); byte[] data = agent.getExecutionData(resetAgent); if(sessionId != null) { log.info("Setting JaCoCo sessionId={}", sessionId); agent.setSessionId(sessionId); } resp.getOutputStream().write(data); resp.getOutputStream().flush(); } } private String getUsageInfo() { return new StringBuilder() .append("This is ") .append(getClass().getName()) .append("\n\n") .append("To get the jacoco data, use " + servletPath + EXEC_PATH) .append("\n\n") .append(EXPLAIN) .toString(); } /** * Lookup the jacoco agent mbean and return it if it exists. Return null otherwise. * @return jacoco agent MBean if registered, null if it is not registered */ private IAgent getAgent() { MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); try { ObjectName name = new ObjectName(JMX_NAME); if (mbs.isRegistered(name)) { return MBeanServerInvocationHandler.newProxyInstance(mbs, name, IAgent.class, false); } } catch (MalformedObjectNameException e) { log.error("[getAgent] there is a typo in the JMX_NAME constant", e); } return null; } }