blob: 5d7d5745173c01cb72108242ca160f64ffcddb35 [file] [log] [blame]
/*
* 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;
}
}