AgentOptions.java

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