AgentOptions.java revision 37115f4ba4f6126b8c3352ac890c653e428a7dd8
1/*******************************************************************************
2 * Copyright (c) 2009, 2013 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 *******************************************************************************/
12package org.jacoco.core.runtime;
13
14import static java.lang.String.format;
15
16import java.io.File;
17import java.util.Arrays;
18import java.util.Collection;
19import java.util.HashMap;
20import java.util.Map;
21import java.util.Properties;
22
23/**
24 * Utility to create and parse options for the runtime agent. Options are
25 * represented as a string in the following format:
26 *
27 * <pre>
28 *   key1=value1,key2=value2,key3=value3
29 * </pre>
30 */
31public final class AgentOptions {
32
33	/**
34	 * Specifies the output file for execution data. Default is
35	 * <code>jacoco.exec</code> in the working directory.
36	 */
37	public static final String DESTFILE = "destfile";
38
39	/**
40	 * Specifies whether execution data should be appended to the output file.
41	 * Default is <code>true</code>.
42	 */
43	public static final String APPEND = "append";
44
45	/**
46	 * Wildcard expression for class names that should be included for code
47	 * coverage. Default is <code>*</code> (all classes included).
48	 *
49	 * @see WildcardMatcher
50	 */
51	public static final String INCLUDES = "includes";
52
53	/**
54	 * Wildcard expression for class names that should be excluded from code
55	 * coverage. Default is the empty string (no exclusions).
56	 *
57	 * @see WildcardMatcher
58	 */
59	public static final String EXCLUDES = "excludes";
60
61	/**
62	 * Wildcard expression for class loaders names for classes that should be
63	 * excluded from code coverage. This means all classes loaded by a class
64	 * loader which full qualified name matches this expression will be ignored
65	 * for code coverage regardless of all other filtering settings. Default is
66	 * <code>sun.reflect.DelegatingClassLoader</code>.
67	 *
68	 * @see WildcardMatcher
69	 */
70	public static final String EXCLCLASSLOADER = "exclclassloader";
71
72	/**
73	 * Specifies a session identifier that is written with the execution data.
74	 * Without this parameter a random identifier is created by the agent.
75	 */
76	public static final String SESSIONID = "sessionid";
77
78	/**
79	 * Specifies whether the agent will automatically dump coverage data on VM
80	 * exit. Default is <code>true</code>.
81	 */
82	public static final String DUMPONEXIT = "dumponexit";
83
84	/**
85	 * Specifies the output mode. Default is
86	 * <code>{@link OutputMode#file}</code>.
87	 *
88	 * @see OutputMode#file
89	 * @see OutputMode#tcpserver
90	 * @see OutputMode#tcpclient
91	 */
92	public static final String OUTPUT = "output";
93
94	/**
95	 * Possible values for {@link AgentOptions#OUTPUT}.
96	 */
97	public static enum OutputMode {
98
99		/**
100		 * Value for the {@link AgentOptions#OUTPUT} parameter: At VM
101		 * termination execution data is written to the file specified by
102		 * {@link AgentOptions#DESTFILE}.
103		 */
104		file,
105
106		/**
107		 * Value for the {@link AgentOptions#OUTPUT} parameter: The agent
108		 * listens for incoming connections on a TCP port specified by
109		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT}.
110		 */
111		tcpserver,
112
113		/**
114		 * Value for the {@link AgentOptions#OUTPUT} parameter: At startup the
115		 * agent connects to a TCP port specified by the
116		 * {@link AgentOptions#ADDRESS} and {@link AgentOptions#PORT} attribute.
117		 */
118		tcpclient,
119
120		/**
121		 * Value for the {@link AgentOptions#OUTPUT} parameter: At startup the
122		 * agent creates MBean.
123		 */
124		mbean
125
126	}
127
128	/**
129	 * The IP address or DNS name the tcpserver binds to or the tcpclient
130	 * connects to. Default is defined by {@link #DEFAULT_ADDRESS}.
131	 */
132	public static final String ADDRESS = "address";
133
134	/**
135	 * Default value for the "address" agent option.
136	 */
137	public static final String DEFAULT_ADDRESS = null;
138
139	/**
140	 * The port the tcpserver binds to or the tcpclient connects to. In
141	 * tcpserver mode the port must be available, which means that if multiple
142	 * JaCoCo agents should run on the same machine, different ports have to be
143	 * specified. Default is defined by {@link #DEFAULT_PORT}.
144	 */
145	public static final String PORT = "port";
146
147	/**
148	 * Default value for the "port" agent option.
149	 */
150	public static final int DEFAULT_PORT = 6300;
151
152	/**
153	 * Specifies where the agent dumps all class files it encounters. The
154	 * location is specified as a relative path to the working directory.
155	 * Default is <code>null</code> (no dumps).
156	 */
157	public static final String CLASSDUMPDIR = "classdumpdir";
158
159	private static final Collection<String> VALID_OPTIONS = Arrays.asList(
160			DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER, SESSIONID,
161			DUMPONEXIT, OUTPUT, ADDRESS, PORT, CLASSDUMPDIR);
162
163	private final Map<String, String> options;
164
165	/**
166	 * New instance with all values set to default.
167	 */
168	public AgentOptions() {
169		this.options = new HashMap<String, String>();
170	}
171
172	/**
173	 * New instance parsed from the given option string.
174	 *
175	 * @param optionstr
176	 *            string to parse or <code>null</code>
177	 */
178	public AgentOptions(final String optionstr) {
179		this();
180		if (optionstr != null && optionstr.length() > 0) {
181			for (final String entry : optionstr.split(",")) {
182				final int pos = entry.indexOf('=');
183				if (pos == -1) {
184					throw new IllegalArgumentException(format(
185							"Invalid agent option syntax \"%s\".", optionstr));
186				}
187				final String key = entry.substring(0, pos);
188				if (!VALID_OPTIONS.contains(key)) {
189					throw new IllegalArgumentException(format(
190							"Unknown agent option \"%s\".", key));
191				}
192
193				final String value = entry.substring(pos + 1);
194				setOption(key, value);
195			}
196
197			validateAll();
198		}
199	}
200
201	/**
202	 * New instance read from the given {@link Properties} object.
203	 *
204	 * @param properties
205	 *            {@link Properties} object to read configuration options from
206	 */
207	public AgentOptions(final Properties properties) {
208		this();
209		for (final String key : VALID_OPTIONS) {
210			final String value = properties.getProperty(key);
211			if (value != null) {
212				setOption(key, value);
213			}
214		}
215	}
216
217	private void validateAll() {
218		validatePort(getPort());
219		getOutput();
220	}
221
222	private void validatePort(final int port) {
223		if (port < 0) {
224			throw new IllegalArgumentException("port must be positive");
225		}
226	}
227
228	/**
229	 * Returns the output file location.
230	 *
231	 * @return output file location
232	 */
233	public String getDestfile() {
234		return getOption(DESTFILE, "jacoco.exec");
235	}
236
237	/**
238	 * Sets the output file location.
239	 *
240	 * @param destfile
241	 *            output file location
242	 */
243	public void setDestfile(final String destfile) {
244		setOption(DESTFILE, destfile);
245	}
246
247	/**
248	 * Returns whether the output should be appended to an existing file.
249	 *
250	 * @return <code>true</code>, when the output should be appended
251	 */
252	public boolean getAppend() {
253		return getOption(APPEND, true);
254	}
255
256	/**
257	 * Sets whether the output should be appended to an existing file.
258	 *
259	 * @param append
260	 *            <code>true</code>, when the output should be appended
261	 */
262	public void setAppend(final boolean append) {
263		setOption(APPEND, append);
264	}
265
266	/**
267	 * Returns the wildcard expression for classes to include.
268	 *
269	 * @return wildcard expression for classes to include
270	 * @see WildcardMatcher
271	 */
272	public String getIncludes() {
273		return getOption(INCLUDES, "*");
274	}
275
276	/**
277	 * Sets the wildcard expression for classes to include.
278	 *
279	 * @param includes
280	 *            wildcard expression for classes to include
281	 * @see WildcardMatcher
282	 */
283	public void setIncludes(final String includes) {
284		setOption(INCLUDES, includes);
285	}
286
287	/**
288	 * Returns the wildcard expression for classes to exclude.
289	 *
290	 * @return wildcard expression for classes to exclude
291	 * @see WildcardMatcher
292	 */
293	public String getExcludes() {
294		return getOption(EXCLUDES, "");
295	}
296
297	/**
298	 * Sets the wildcard expression for classes to exclude.
299	 *
300	 * @param excludes
301	 *            wildcard expression for classes to exclude
302	 * @see WildcardMatcher
303	 */
304	public void setExcludes(final String excludes) {
305		setOption(EXCLUDES, excludes);
306	}
307
308	/**
309	 * Returns the wildcard expression for excluded class loaders.
310	 *
311	 * @return expression for excluded class loaders
312	 * @see WildcardMatcher
313	 */
314	public String getExclClassloader() {
315		return getOption(EXCLCLASSLOADER, "sun.reflect.DelegatingClassLoader");
316	}
317
318	/**
319	 * Sets the wildcard expression for excluded class loaders.
320	 *
321	 * @param expression
322	 *            expression for excluded class loaders
323	 * @see WildcardMatcher
324	 */
325	public void setExclClassloader(final String expression) {
326		setOption(EXCLCLASSLOADER, expression);
327	}
328
329	/**
330	 * Returns the session identifier.
331	 *
332	 * @return session identifier
333	 */
334	public String getSessionId() {
335		return getOption(SESSIONID, null);
336	}
337
338	/**
339	 * Sets the session identifier.
340	 *
341	 * @param id
342	 *            session identifier
343	 */
344	public void setSessionId(final String id) {
345		setOption(SESSIONID, id);
346	}
347
348	/**
349	 * Returns whether coverage data should be dumped on exit
350	 *
351	 * @return <code>true</code> if coverage data will be written on VM exit
352	 */
353	public boolean getDumpOnExit() {
354		return getOption(DUMPONEXIT, true);
355	}
356
357	/**
358	 * Sets whether coverage data should be dumped on exit
359	 *
360	 * @param dumpOnExit
361	 *            <code>true</code> if coverage data should be written on VM
362	 *            exit
363	 */
364	public void setDumpOnExit(final boolean dumpOnExit) {
365		setOption(DUMPONEXIT, dumpOnExit);
366	}
367
368	/**
369	 * Returns the port on which to listen to when the output is
370	 * <code>tcpserver</code> or the port to connect to when output is
371	 * <code>tcpclient</code>.
372	 *
373	 * @return port to listen on or connect to
374	 */
375	public int getPort() {
376		return getOption(PORT, DEFAULT_PORT);
377	}
378
379	/**
380	 * Sets the port on which to listen to when output is <code>tcpserver</code>
381	 * or the port to connect to when output is <code>tcpclient</code>
382	 *
383	 * @param port
384	 */
385	public void setPort(final int port) {
386		validatePort(port);
387		setOption(PORT, port);
388	}
389
390	/**
391	 * Gets the hostname or IP address to listen to when output is
392	 * <code>tcpserver</code> or connect to when output is
393	 * <code>tcpclient</code>
394	 *
395	 * @return Hostname or IP address
396	 */
397	public String getAddress() {
398		return getOption(ADDRESS, DEFAULT_ADDRESS);
399	}
400
401	/**
402	 * Sets the hostname or IP address to listen to when output is
403	 * <code>tcpserver</cose> or connect to when output is <code>tcpclient</code>
404	 *
405	 * @param address
406	 *            Hostname or IP address
407	 */
408	public void setAddress(final String address) {
409		setOption(ADDRESS, address);
410	}
411
412	/**
413	 * Returns the output mode
414	 *
415	 * @return current output mode
416	 */
417	public OutputMode getOutput() {
418		final String value = options.get(OUTPUT);
419		return value == null ? OutputMode.file : OutputMode.valueOf(value);
420	}
421
422	/**
423	 * Sets the output mode
424	 *
425	 * @param output
426	 *            Output mode
427	 */
428	public void setOutput(final String output) {
429		setOutput(OutputMode.valueOf(output));
430	}
431
432	/**
433	 * Sets the output mode
434	 *
435	 * @param output
436	 *            Output mode
437	 */
438	public void setOutput(final OutputMode output) {
439		setOption(OUTPUT, output.name());
440	}
441
442	/**
443	 * Returns the location of the directory where class files should be dumped
444	 * to.
445	 *
446	 * @return dump location or <code>null</code> (no dumps)
447	 */
448	public String getClassDumpDir() {
449		return getOption(CLASSDUMPDIR, null);
450	}
451
452	/**
453	 * Sets the directory where class files should be dumped to.
454	 *
455	 * @param location
456	 *            dump location or <code>null</code> (no dumps)
457	 */
458	public void setClassDumpDir(final String location) {
459		setOption(CLASSDUMPDIR, location);
460	}
461
462	private void setOption(final String key, final int value) {
463		setOption(key, Integer.toString(value));
464	}
465
466	private void setOption(final String key, final boolean value) {
467		setOption(key, Boolean.toString(value));
468	}
469
470	private void setOption(final String key, final String value) {
471		if (value.contains(",")) {
472			throw new IllegalArgumentException(format(
473					"Invalid character in option argument \"%s\"", value));
474		}
475		options.put(key, value);
476	}
477
478	private String getOption(final String key, final String defaultValue) {
479		final String value = options.get(key);
480		return value == null ? defaultValue : value;
481	}
482
483	private boolean getOption(final String key, final boolean defaultValue) {
484		final String value = options.get(key);
485		return value == null ? defaultValue : Boolean.parseBoolean(value);
486	}
487
488	private int getOption(final String key, final int defaultValue) {
489		final String value = options.get(key);
490		return value == null ? defaultValue : Integer.parseInt(value);
491	}
492
493	/**
494	 * Generate required JVM argument string based on current configuration and
495	 * supplied agent jar location
496	 *
497	 * @param agentJarFile
498	 *            location of the JaCoCo Agent Jar
499	 * @return Argument to pass to create new VM with coverage enabled
500	 */
501	public String getVMArgument(final File agentJarFile) {
502		return format("-javaagent:%s=%s", agentJarFile, this);
503	}
504
505	/**
506	 * Creates a string representation that can be passed to the agent via the
507	 * command line. Might be the empty string, if no options are set.
508	 */
509	@Override
510	public String toString() {
511		final StringBuilder sb = new StringBuilder();
512		for (final String key : VALID_OPTIONS) {
513			final String value = options.get(key);
514			if (value != null) {
515				if (sb.length() > 0) {
516					sb.append(',');
517				}
518				sb.append(key).append('=').append(value);
519			}
520		}
521		return sb.toString();
522	}
523
524}
525