My first Java Application (apache commons cli) #1

Recently I’ve started to write my first Java-Application. I’m developing a before-queue-policy filter for Postfix (aka smtpd-proxy) which allows to filter whole mails before they’re stored in postfix‘ queue. I’ll document my process of learning Java and writing that policy daemon here 🙂 Alright, I have to admit that I already have some experience with Java and due to my programming background the syntax isn’t very difficult to me. Also some friends are helping sometimes (thanks to Till, Stefan and Sven).

Alright. Stefan has shown me how to use Maven within Intellij Idea (That’s the IDE I’m using). Maven is a pretty nice tool – It fetches all dependencies (and their dependencies) automatically and hence takes care of everything. So what I did is the following:

File -> New Project -> Maven Project

It asks for a GroupId and ArtifactId. In This case I picked the packagename as GroupId (com.example) and the name of my Application (LyraPolicy) as ArtifactId. After clicking „finish“ the file pom.xml is opened; You can add dependencies by adding a dependencies block below the „version“ field.

<dependencies>
        <dependency>
            <groupId>commons-cli</groupId>
            <artifactId>commons-cli</artifactId>
            <version>1.2</version>
        </dependency>
</dependencies>

You may also create a developer block and similar things. Introduction and examples to the POM can be found here: http://maven.apache.org/guides/introduction/introduction-to-the-pom.html. As soon as you added a dependency like that, a notification will appear on the top right, that one tells you that something needs to be imported and that you can enable auto import (which is what I did) – now every time you add a dependency, it will be added automatically.

Let’s go on. On the left in Idea display the contents of the src folder, then the contents of the main folder and right click the java folder:

New -> Package -> com.example.lyrapolicy

Right click that new folder (package) and do:

New -> Java Class -> Main

That will open a new file with the class Main. This already leads to one interesting aspect in Java by the way. You might name it differently (It does not need to be named Main, though this seems to be often the case). You HAVE to have a main method in at least one of your classes – Thats the method which is run when you start your Java application. Hence I’ll create that in the Main class. The interesting aspect I referred to is about the constructor: If you’re writing a constructor in PHP the method is named __construct(). In Java on the contrary it’s the class name. That means, the constructor of the class Main is called „Main“. The constructor of the class Foo is called „Foo“.

Please note, that the „entry“-point of you application always has to be of the form „public static void main(String[] args)“ and if you use a constructor it has no return type.

I want to give my program a few parameters:

  • -i IP to listen on
  • -p Port to listen on
  • -c Configuration file
  • -v Verbose output // debugging
  • -tp Threads
  • -tt Timeout per thread
  • -h help

I’ve seen that there’s a library at Apache called „commons-cli“ which can be used for this stuff. If you took a look at my dependency list in the pom.xml (start of this article) you’ve noticed that I listed that library already. The following examples can be used for simple boolean options, they’re true if set, false if not.

Option help = new Option("h", "prints this message");
Option verbose = new Option("v", "verbose output");

More complex options with arguments are possible as well:

Option ip = OptionBuilder.withArgName("ip addr")
            .hasArg()
            .withDescription("ip address to listen on")
            .create("i");

This will create an option with name „i“ with the argument „ip addr“. One you’ve set all options like that, you can add them:

Options options = new Options();
options.addOption(help);
options.addOption(verbose);
options.addOption(ip);

The whole code so far:

String serverAddr = "127.0.0.1";
String serverPort = "4444";
String verbose = "";
 
Option serverAddrOption = OptionBuilder
       .withArgName("addr")
       .hasArg()
       .withDescription("IP address to listen on")
       .create("ip");
Option serverPortOption = OptionBuilder
       .withArgName("port")
       .hasArg()
       .withDescription("port to listen on")
       .create("port");
Option help = new Option("help", "print this message");
Option verboseOption = new Option("verbose", "verbose output");
 
Options options = new Options();
options.addOption(serverAddrOption);
options.addOption(serverPortOption);
options.addOption(verboseOption);
options.addOption(help);

What’s missing now? Ah yeah. The „parser“ and CommandLineInterface. Here’s how I did it (pretty much copied from the example page and enhanced to fit my needs):

CommandLineParser parser = new GnuParser();
try
{
       CommandLine line = parser.parse(options, args);
       if (line.hasOption("help"))
       {
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp("LyraPolicy", options);
       } else
       {
            if (line.hasOption("addr"))
            {
                serverAddr = line.getOptionValue("addr");
            }
            if (line.hasOption("port"))
            {
                serverPort = line.getOptionValue("port");
            }
            if (line.hasOption("verbose"))
            {
                verbose = line.getOptionValue("verbose");
            }
 
            // the following is DEBUG because I don't know
            // how to tell IDEA that it should start this with
            // arguments. :)
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp("LyraPolicy", options);
            System.out.println(serverAddr + " " + serverPort + " " + verbose);
        }
} catch (ParseException exp)
{
    // oops, something went wrong
    System.err.println("Parsing failed. Reason: " + exp.getMessage());
}

That will result in:

usage: LyraPolicy
 -help          print this message
 -ip <addr>     IP address to listen on
 -port <port>   port to listen on
 -verbose       verbose output
127.0.0.1 4444

Pretty nice. So I’ve got the commandline already. Now I want to parse a properties file as configuration file. That’s pretty easy. A properties file stores stuff like this:

addr=127.0.0.1

You can parse such a file like this:

configFile = line.getOptionValue("c");
Properties props = new Properties();
FileInputStream in = null;
 
try
{
    in = new FileInputStream(configFile);
    props.load(in);
    in.close();
} catch (IOException e)
{
     e.printStackTrace();
}
 
if (props.getProperty("addr") != null)
{
    serverAddress = props.getProperty("addr");
}

The whole file so far:

package com.example.lyrapolicy;
 
 
import org.apache.commons.cli.*;
 
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
 
public class Main
{
    /**
     * Starting point
     *
     * @param args
     */
    public static void main(String[] args)
    {
        /**
         * Defaults
         */
        String serverAddress = "127.0.0.1";
        int serverPort = 4444;
        String configFile = "";
        int serverThreads = 2;
        int serverThreadTimeout = 100;
        boolean verboseOutput = false;
 
        /**
         * CLI Options
         */
        Option help = new Option("h", "prints this message");
        Option verbose = new Option("v", "verbose output");
        Option ip = OptionBuilder.withArgName("addr")
                .hasArg()
                .withDescription("ip address to listen on [127.0.0.1]")
                .create("i");
        Option port = OptionBuilder.withArgName("port")
                .hasArg()
                .withDescription("port to listen on [4444]")
                .create("p");
        Option config = OptionBuilder.withArgName("file")
                .hasArg()
                .withDescription("configuration file to use")
                .create("c");
        Option threads = OptionBuilder.withArgName("threads")
                .hasArg()
                .withDescription("threads [10]")
                .create("tp");
        Option threadTimeout = OptionBuilder.withArgName("timeout")
                .hasArg()
                .withDescription("timeout per thread [100] seconds")
                .create("tt");
 
        Options options = new Options();
        options.addOption(help);
        options.addOption(verbose);
        options.addOption(ip);
        options.addOption(port);
        options.addOption(config);
        options.addOption(threads);
        options.addOption(threadTimeout);
 
        /**
         * CLI Parser
         */
        CommandLineParser parser = new GnuParser();
        try
        {
            CommandLine line = parser.parse(options, args);
            if (line.hasOption("h"))
            {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printHelp("LyraPolicy", options);
            } else
            {
                /**
                 * if there is a config file, it will override
                 * the defaults in this file using it's values.
                 * the command line options will override the
                 * values from the config file
                 */
                if (line.hasOption("c"))
                {
                    configFile = line.getOptionValue("c");
                    Properties props = new Properties();
                    FileInputStream in = null;
 
                    try
                    {
                        in = new FileInputStream(configFile);
                        props.load(in);
                        in.close();
                    } catch (IOException e)
                    {
                        e.printStackTrace();
                    }
 
                    if (props.getProperty("addr") != null)
                    {
                        serverAddress = props.getProperty("addr");
                    }
 
                    if (props.getProperty("port") != null)
                    {
                        serverPort = Integer.parseInt(props.getProperty("port"));
                    }
 
                    if (props.getProperty("threads") != null)
                    {
                        serverThreads = Integer.parseInt(props.getProperty("threads"));
                    }
 
                    if (props.getProperty("threadtimeout") != null)
                    {
                        serverThreadTimeout = Integer.parseInt(props.getProperty("threadtimeout"));
                    }
 
                    if (props.getProperty("verbose") != null)
                    {
                        verboseOutput = true;
                    }
                }
 
                if (line.hasOption("i"))
                {
                    serverAddress = line.getOptionValue("i");
                }
 
                if (line.hasOption("p"))
                {
                    serverPort = Integer.parseInt(line.getOptionValue("p"));
                }
 
                if (line.hasOption("v"))
                {
                    verboseOutput = true;
                }
 
                if (line.hasOption("tp"))
                {
                    serverThreads = Integer.parseInt(line.getOptionValue("t"));
                }
 
                if (line.hasOption("tt"))
                {
                    serverThreadTimeout = Integer.parseInt(line.getOptionValue("m"));
                }
 
                // here we'll start the "server" later.
                Server server = new Server(serverAddress, serverPort, serverThreads, serverThreadTimeout, verboseOutput);
                server.run();
            }
        } catch (ParseException exp)
        {
            // oops, something went wrong
            System.err.println("Parsing failed. Reason: " + exp.getMessage());
        }
    }
}
usage: LyraPolicy
 -c <file>       configuration file to use
 -h              prints this message
 -i <addr>       ip address to listen on [127.0.0.1]
 -p <port>       port to listen on [4444]
 -tp <threads>   threads [10]
 -tt <timeout>   timeout per thread [100] seconds
 -v              verbose output

No Comments

Post a Comment