1 /*******************************************************************************
2  * Copyright (c) 2009, 2015 Mountainminds GmbH & Co. KG and Contributors
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/epl-v10.html
7  *
8  * Contributors:
9  *    Marc R. Hoffmann - initial API and implementation
10  *
11  *******************************************************************************/
12 package org.jacoco.core.runtime;
13 
14 import static java.lang.String.format;
15 
16 import java.io.File;
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.HashMap;
20 import java.util.Iterator;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Properties;
24 
25 /**
26  * Utility to create and parse options for the runtime agent. Options are
27  * represented as a string in the following format:
28  *
29  * <pre>
30  *   key1=value1,key2=value2,key3=value3
31  * </pre>
32  */
33 public final class AgentOptions {
34 
35 	/**
36 	 * Specifies the output file for execution data. Default is
37 	 * <code>jacoco.exec</code> in the working directory.
38 	 */
39 	public static final String DESTFILE = "destfile";
40 
41 	/**
42 	 * Default value for the "destfile" agent option.
43 	 */
44 	public static final String DEFAULT_DESTFILE = "jacoco.exec";
45 
46 	/**
47 	 * Specifies whether execution data should be appended to the output file.
48 	 * Default is <code>true</code>.
49 	 */
50 	public static final String APPEND = "append";
51 
52 	/**
53 	 * Wildcard expression for class names that should be included for code
54 	 * coverage. Default is <code>*</code> (all classes included).
55 	 *
56 	 * @see WildcardMatcher
57 	 */
58 	public static final String INCLUDES = "includes";
59 
60 	/**
61 	 * Wildcard expression for class names that should be excluded from code
62 	 * coverage. Default is the empty string (no exclusions).
63 	 *
64 	 * @see WildcardMatcher
65 	 */
66 	public static final String EXCLUDES = "excludes";
67 
68 	/**
69 	 * Wildcard expression for class loaders names for classes that should be
70 	 * excluded from code coverage. This means all classes loaded by a class
71 	 * loader which full qualified name matches this expression will be ignored
72 	 * for code coverage regardless of all other filtering settings. Default is
73 	 * <code>sun.reflect.DelegatingClassLoader</code>.
74 	 *
75 	 * @see WildcardMatcher
76 	 */
77 	public static final String EXCLCLASSLOADER = "exclclassloader";
78 
79 	/**
80 	 * Specifies whether also classes from the bootstrap classloader should be
81 	 * instrumented. Use this feature with caution, it needs heavy
82 	 * includes/excludes tuning. Default is <code>false</code>.
83 	 */
84 	public static final String INCLBOOTSTRAPCLASSES = "inclbootstrapclasses";
85 
86 	/**
87 	 * Specifies a session identifier that is written with the execution data.
88 	 * Without this parameter a random identifier is created by the agent.
89 	 */
90 	public static final String SESSIONID = "sessionid";
91 
92 	/**
93 	 * Specifies whether the agent will automatically dump coverage data on VM
94 	 * exit. Default is <code>true</code>.
95 	 */
96 	public static final String DUMPONEXIT = "dumponexit";
97 
98 	/**
99 	 * Specifies the output mode. Default is {@link OutputMode#file}.
100 	 *
101 	 * @see OutputMode#file
102 	 * @see OutputMode#tcpserver
103 	 * @see OutputMode#tcpclient
104 	 * @see OutputMode#none
105 	 */
106 	public static final String OUTPUT = "output";
107 
108 	/**
109 	 * Possible values for {@link AgentOptions#OUTPUT}.
110 	 */
111 	public static enum OutputMode {
112 
113 		/**
114 		 * Value for the {@link AgentOptions#OUTPUT} parameter: At VM
115 		 * termination execution data is written to the file specified by
116 		 * {@link AgentOptions#DESTFILE}.
117 		 */
118 		file,
119 
120 		/**
121 		 * Value for the {@link AgentOptions#OUTPUT} parameter: The agent
122 		 * listens for incoming connections on a TCP port specified by
123 		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT}.
124 		 */
125 		tcpserver,
126 
127 		/**
128 		 * Value for the {@link AgentOptions#OUTPUT} parameter: At startup the
129 		 * agent connects to a TCP port specified by the
130 		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT} attribute.
131 		 */
132 		tcpclient,
133 
134 		/**
135 		 * Value for the {@link AgentOptions#OUTPUT} parameter: Do not produce
136 		 * any output.
137 		 */
138 		none
139 
140 	}
141 
142 	/**
143 	 * The IP address or DNS name the tcpserver binds to or the tcpclient
144 	 * connects to. Default is defined by {@link #DEFAULT_ADDRESS}.
145 	 */
146 	public static final String ADDRESS = "address";
147 
148 	/**
149 	 * Default value for the "address" agent option.
150 	 */
151 	public static final String DEFAULT_ADDRESS = null;
152 
153 	/**
154 	 * The port the tcpserver binds to or the tcpclient connects to. In
155 	 * tcpserver mode the port must be available, which means that if multiple
156 	 * JaCoCo agents should run on the same machine, different ports have to be
157 	 * specified. Default is defined by {@link #DEFAULT_PORT}.
158 	 */
159 	public static final String PORT = "port";
160 
161 	/**
162 	 * Default value for the "port" agent option.
163 	 */
164 	public static final int DEFAULT_PORT = 6300;
165 
166 	/**
167 	 * Specifies where the agent dumps all class files it encounters. The
168 	 * location is specified as a relative path to the working directory.
169 	 * Default is <code>null</code> (no dumps).
170 	 */
171 	public static final String CLASSDUMPDIR = "classdumpdir";
172 
173 	/**
174 	 * Specifies whether the agent should expose functionality via JMX under the
175 	 * name "org.jacoco:type=Runtime". Default is <code>false</code>.
176 	 */
177 	public static final String JMX = "jmx";
178 
179 	private static final Collection<String> VALID_OPTIONS = Arrays.asList(
180 			DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER,
181 			INCLBOOTSTRAPCLASSES, SESSIONID, DUMPONEXIT, OUTPUT, ADDRESS, PORT,
182 			CLASSDUMPDIR, JMX);
183 
184 	private final Map<String, String> options;
185 
186 	/**
187 	 * New instance with all values set to default.
188 	 */
AgentOptions()189 	public AgentOptions() {
190 		this.options = new HashMap<String, String>();
191 	}
192 
193 	/**
194 	 * New instance parsed from the given option string.
195 	 *
196 	 * @param optionstr
197 	 *            string to parse or <code>null</code>
198 	 */
AgentOptions(final String optionstr)199 	public AgentOptions(final String optionstr) {
200 		this();
201 		if (optionstr != null && optionstr.length() > 0) {
202 			for (final String entry : optionstr.split(",")) {
203 				final int pos = entry.indexOf('=');
204 				if (pos == -1) {
205 					throw new IllegalArgumentException(format(
206 							"Invalid agent option syntax \"%s\".", optionstr));
207 				}
208 				final String key = entry.substring(0, pos);
209 				if (!VALID_OPTIONS.contains(key)) {
210 					throw new IllegalArgumentException(format(
211 							"Unknown agent option \"%s\".", key));
212 				}
213 
214 				final String value = entry.substring(pos + 1);
215 				setOption(key, value);
216 			}
217 
218 			validateAll();
219 		}
220 	}
221 
222 	/**
223 	 * New instance read from the given {@link Properties} object.
224 	 *
225 	 * @param properties
226 	 *            {@link Properties} object to read configuration options from
227 	 */
AgentOptions(final Properties properties)228 	public AgentOptions(final Properties properties) {
229 		this();
230 		for (final String key : VALID_OPTIONS) {
231 			final String value = properties.getProperty(key);
232 			if (value != null) {
233 				setOption(key, value);
234 			}
235 		}
236 	}
237 
validateAll()238 	private void validateAll() {
239 		validatePort(getPort());
240 		getOutput();
241 	}
242 
validatePort(final int port)243 	private void validatePort(final int port) {
244 		if (port < 0) {
245 			throw new IllegalArgumentException("port must be positive");
246 		}
247 	}
248 
249 	/**
250 	 * Returns the output file location.
251 	 *
252 	 * @return output file location
253 	 */
getDestfile()254 	public String getDestfile() {
255 		return getOption(DESTFILE, DEFAULT_DESTFILE);
256 	}
257 
258 	/**
259 	 * Sets the output file location.
260 	 *
261 	 * @param destfile
262 	 *            output file location
263 	 */
setDestfile(final String destfile)264 	public void setDestfile(final String destfile) {
265 		setOption(DESTFILE, destfile);
266 	}
267 
268 	/**
269 	 * Returns whether the output should be appended to an existing file.
270 	 *
271 	 * @return <code>true</code>, when the output should be appended
272 	 */
getAppend()273 	public boolean getAppend() {
274 		return getOption(APPEND, true);
275 	}
276 
277 	/**
278 	 * Sets whether the output should be appended to an existing file.
279 	 *
280 	 * @param append
281 	 *            <code>true</code>, when the output should be appended
282 	 */
setAppend(final boolean append)283 	public void setAppend(final boolean append) {
284 		setOption(APPEND, append);
285 	}
286 
287 	/**
288 	 * Returns the wildcard expression for classes to include.
289 	 *
290 	 * @return wildcard expression for classes to include
291 	 * @see WildcardMatcher
292 	 */
getIncludes()293 	public String getIncludes() {
294 		return getOption(INCLUDES, "*");
295 	}
296 
297 	/**
298 	 * Sets the wildcard expression for classes to include.
299 	 *
300 	 * @param includes
301 	 *            wildcard expression for classes to include
302 	 * @see WildcardMatcher
303 	 */
setIncludes(final String includes)304 	public void setIncludes(final String includes) {
305 		setOption(INCLUDES, includes);
306 	}
307 
308 	/**
309 	 * Returns the wildcard expression for classes to exclude.
310 	 *
311 	 * @return wildcard expression for classes to exclude
312 	 * @see WildcardMatcher
313 	 */
getExcludes()314 	public String getExcludes() {
315 		return getOption(EXCLUDES, "");
316 	}
317 
318 	/**
319 	 * Sets the wildcard expression for classes to exclude.
320 	 *
321 	 * @param excludes
322 	 *            wildcard expression for classes to exclude
323 	 * @see WildcardMatcher
324 	 */
setExcludes(final String excludes)325 	public void setExcludes(final String excludes) {
326 		setOption(EXCLUDES, excludes);
327 	}
328 
329 	/**
330 	 * Returns the wildcard expression for excluded class loaders.
331 	 *
332 	 * @return expression for excluded class loaders
333 	 * @see WildcardMatcher
334 	 */
getExclClassloader()335 	public String getExclClassloader() {
336 		return getOption(EXCLCLASSLOADER, "sun.reflect.DelegatingClassLoader");
337 	}
338 
339 	/**
340 	 * Sets the wildcard expression for excluded class loaders.
341 	 *
342 	 * @param expression
343 	 *            expression for excluded class loaders
344 	 * @see WildcardMatcher
345 	 */
setExclClassloader(final String expression)346 	public void setExclClassloader(final String expression) {
347 		setOption(EXCLCLASSLOADER, expression);
348 	}
349 
350 	/**
351 	 * Returns whether classes from the bootstrap classloader should be
352 	 * instrumented.
353 	 *
354 	 * @return <code>true</code> if coverage data will be written on VM exit
355 	 */
getInclBootstrapClasses()356 	public boolean getInclBootstrapClasses() {
357 		return getOption(INCLBOOTSTRAPCLASSES, false);
358 	}
359 
360 	/**
361 	 * Sets whether classes from the bootstrap classloader should be
362 	 * instrumented.
363 	 *
364 	 * @param include
365 	 *            <code>true</code> if bootstrap classes should be instrumented
366 	 */
setInclBootstrapClasses(final boolean include)367 	public void setInclBootstrapClasses(final boolean include) {
368 		setOption(INCLBOOTSTRAPCLASSES, include);
369 	}
370 
371 	/**
372 	 * Returns the session identifier.
373 	 *
374 	 * @return session identifier
375 	 */
getSessionId()376 	public String getSessionId() {
377 		return getOption(SESSIONID, null);
378 	}
379 
380 	/**
381 	 * Sets the session identifier.
382 	 *
383 	 * @param id
384 	 *            session identifier
385 	 */
setSessionId(final String id)386 	public void setSessionId(final String id) {
387 		setOption(SESSIONID, id);
388 	}
389 
390 	/**
391 	 * Returns whether coverage data should be dumped on exit.
392 	 *
393 	 * @return <code>true</code> if coverage data will be written on VM exit
394 	 */
getDumpOnExit()395 	public boolean getDumpOnExit() {
396 		return getOption(DUMPONEXIT, true);
397 	}
398 
399 	/**
400 	 * Sets whether coverage data should be dumped on exit.
401 	 *
402 	 * @param dumpOnExit
403 	 *            <code>true</code> if coverage data should be written on VM
404 	 *            exit
405 	 */
setDumpOnExit(final boolean dumpOnExit)406 	public void setDumpOnExit(final boolean dumpOnExit) {
407 		setOption(DUMPONEXIT, dumpOnExit);
408 	}
409 
410 	/**
411 	 * Returns the port on which to listen to when the output is
412 	 * <code>tcpserver</code> or the port to connect to when output is
413 	 * <code>tcpclient</code>.
414 	 *
415 	 * @return port to listen on or connect to
416 	 */
getPort()417 	public int getPort() {
418 		return getOption(PORT, DEFAULT_PORT);
419 	}
420 
421 	/**
422 	 * Sets the port on which to listen to when output is <code>tcpserver</code>
423 	 * or the port to connect to when output is <code>tcpclient</code>
424 	 *
425 	 * @param port
426 	 *            port to listen on or connect to
427 	 */
setPort(final int port)428 	public void setPort(final int port) {
429 		validatePort(port);
430 		setOption(PORT, port);
431 	}
432 
433 	/**
434 	 * Gets the hostname or IP address to listen to when output is
435 	 * <code>tcpserver</code> or connect to when output is
436 	 * <code>tcpclient</code>
437 	 *
438 	 * @return Hostname or IP address
439 	 */
getAddress()440 	public String getAddress() {
441 		return getOption(ADDRESS, DEFAULT_ADDRESS);
442 	}
443 
444 	/**
445 	 * Sets the hostname or IP address to listen to when output is
446 	 * <code>tcpserver</code> or connect to when output is
447 	 * <code>tcpclient</code>
448 	 *
449 	 * @param address
450 	 *            Hostname or IP address
451 	 */
setAddress(final String address)452 	public void setAddress(final String address) {
453 		setOption(ADDRESS, address);
454 	}
455 
456 	/**
457 	 * Returns the output mode
458 	 *
459 	 * @return current output mode
460 	 */
getOutput()461 	public OutputMode getOutput() {
462 		final String value = options.get(OUTPUT);
463 // BEGIN android-change
464 //		return value == null ? OutputMode.file : OutputMode.valueOf(value);
465 		return value == null ? OutputMode.none : OutputMode.valueOf(value);
466 // END android-change
467 	}
468 
469 	/**
470 	 * Sets the output mode
471 	 *
472 	 * @param output
473 	 *            Output mode
474 	 */
setOutput(final String output)475 	public void setOutput(final String output) {
476 		setOutput(OutputMode.valueOf(output));
477 	}
478 
479 	/**
480 	 * Sets the output mode
481 	 *
482 	 * @param output
483 	 *            Output mode
484 	 */
setOutput(final OutputMode output)485 	public void setOutput(final OutputMode output) {
486 		setOption(OUTPUT, output.name());
487 	}
488 
489 	/**
490 	 * Returns the location of the directory where class files should be dumped
491 	 * to.
492 	 *
493 	 * @return dump location or <code>null</code> (no dumps)
494 	 */
getClassDumpDir()495 	public String getClassDumpDir() {
496 		return getOption(CLASSDUMPDIR, null);
497 	}
498 
499 	/**
500 	 * Sets the directory where class files should be dumped to.
501 	 *
502 	 * @param location
503 	 *            dump location or <code>null</code> (no dumps)
504 	 */
setClassDumpDir(final String location)505 	public void setClassDumpDir(final String location) {
506 		setOption(CLASSDUMPDIR, location);
507 	}
508 
509 	/**
510 	 * Returns whether the agent exposes functionality via JMX.
511 	 *
512 	 * @return <code>true</code>, when JMX is enabled
513 	 */
getJmx()514 	public boolean getJmx() {
515 		return getOption(JMX, false);
516 	}
517 
518 	/**
519 	 * Sets whether the agent should expose functionality via JMX.
520 	 *
521 	 * @param jmx
522 	 *            <code>true</code> if JMX should be enabled
523 	 */
setJmx(final boolean jmx)524 	public void setJmx(final boolean jmx) {
525 		setOption(JMX, jmx);
526 	}
527 
setOption(final String key, final int value)528 	private void setOption(final String key, final int value) {
529 		setOption(key, Integer.toString(value));
530 	}
531 
setOption(final String key, final boolean value)532 	private void setOption(final String key, final boolean value) {
533 		setOption(key, Boolean.toString(value));
534 	}
535 
setOption(final String key, final String value)536 	private void setOption(final String key, final String value) {
537 		if (value.contains(",")) {
538 			throw new IllegalArgumentException(format(
539 					"Invalid character in option argument \"%s\"", value));
540 		}
541 		options.put(key, value);
542 	}
543 
getOption(final String key, final String defaultValue)544 	private String getOption(final String key, final String defaultValue) {
545 		final String value = options.get(key);
546 		return value == null ? defaultValue : value;
547 	}
548 
getOption(final String key, final boolean defaultValue)549 	private boolean getOption(final String key, final boolean defaultValue) {
550 		final String value = options.get(key);
551 		return value == null ? defaultValue : Boolean.parseBoolean(value);
552 	}
553 
getOption(final String key, final int defaultValue)554 	private int getOption(final String key, final int defaultValue) {
555 		final String value = options.get(key);
556 		return value == null ? defaultValue : Integer.parseInt(value);
557 	}
558 
559 	/**
560 	 * Generate required JVM argument based on current configuration and
561 	 * supplied agent jar location.
562 	 *
563 	 * @param agentJarFile
564 	 *            location of the JaCoCo Agent Jar
565 	 * @return Argument to pass to create new VM with coverage enabled
566 	 */
getVMArgument(final File agentJarFile)567 	public String getVMArgument(final File agentJarFile) {
568 		return format("-javaagent:%s=%s", agentJarFile, this);
569 	}
570 
571 	/**
572 	 * Generate required quoted JVM argument based on current configuration and
573 	 * supplied agent jar location.
574 	 *
575 	 * @param agentJarFile
576 	 *            location of the JaCoCo Agent Jar
577 	 * @return Quoted argument to pass to create new VM with coverage enabled
578 	 */
getQuotedVMArgument(final File agentJarFile)579 	public String getQuotedVMArgument(final File agentJarFile) {
580 		return CommandLineSupport.quote(getVMArgument(agentJarFile));
581 	}
582 
583 	/**
584 	 * Generate required quotes JVM argument based on current configuration and
585 	 * prepends it to the given argument command line. If a agent with the same
586 	 * JAR file is already specified this parameter is removed from the existing
587 	 * command line.
588 	 *
589 	 * @param arguments
590 	 *            existing command line arguments or <code>null</code>
591 	 * @param agentJarFile
592 	 *            location of the JaCoCo Agent Jar
593 	 * @return VM command line arguments prepended with configured JaCoCo agent
594 	 */
prependVMArguments(final String arguments, final File agentJarFile)595 	public String prependVMArguments(final String arguments,
596 			final File agentJarFile) {
597 		final List<String> args = CommandLineSupport.split(arguments);
598 		final String plainAgent = format("-javaagent:%s", agentJarFile);
599 		for (final Iterator<String> i = args.iterator(); i.hasNext();) {
600 			if (i.next().startsWith(plainAgent)) {
601 				i.remove();
602 			}
603 		}
604 		args.add(0, getVMArgument(agentJarFile));
605 		return CommandLineSupport.quote(args);
606 	}
607 
608 	/**
609 	 * Creates a string representation that can be passed to the agent via the
610 	 * command line. Might be the empty string, if no options are set.
611 	 */
612 	@Override
toString()613 	public String toString() {
614 		final StringBuilder sb = new StringBuilder();
615 		for (final String key : VALID_OPTIONS) {
616 			final String value = options.get(key);
617 			if (value != null) {
618 				if (sb.length() > 0) {
619 					sb.append(',');
620 				}
621 				sb.append(key).append('=').append(value);
622 			}
623 		}
624 		return sb.toString();
625 	}
626 
627 }
628