CLIProductBuilder.java

/*
 * CLIProductBuilder
 */
package gov.usgs.earthquake.distribution;

import gov.usgs.earthquake.aws.AwsProductSender;
import gov.usgs.earthquake.product.ByteContent;
import gov.usgs.earthquake.product.FileContent;
import gov.usgs.earthquake.product.InputStreamContent;
import gov.usgs.earthquake.product.Product;
import gov.usgs.earthquake.product.ProductId;
import gov.usgs.util.Config;
import gov.usgs.util.CryptoUtils;
import gov.usgs.util.DefaultConfigurable;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.XmlUtils;
import gov.usgs.util.CryptoUtils.Version;
import gov.usgs.util.StringUtils;

import java.io.File;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Command Line Interface Product Builder.
 *
 * This class is used to build and send products. It is typically called by
 * using the --build argument with the standard ProductClient.
 *
 * The CLIProductBuilder implements the Configurable interface and uses the
 * following configuration parameters:
 *
 * <dl>
 * <dt>senders</dt>
 * <dd>(Required). A comma separated list of section names that should be loaded
 * as ProductSender objects. Each sender in this list will be used to send any
 * built products. See each type of ProductSender for more configuration
 * details.</dd>
 * </dl>
 */
public class CLIProductBuilder extends DefaultConfigurable {

	/** Logging object. */
	private static final Logger LOGGER = Logger
			.getLogger(CLIProductBuilder.class.getName());

	/** Exit code used when an invalid combination of arguments is used. */
	public static final int EXIT_INVALID_ARGUMENTS = 1;

	/** Exit code used when unable to build a product. */
	public static final int EXIT_UNABLE_TO_BUILD = 2;

	/** Exit code used when errors occur while sending. */
	public static final int EXIT_UNABLE_TO_SEND = 3;

	/** Exit code when errors occur while sending, but not to all senders. */
	public static final int EXIT_PARTIALLY_SENT = 4;

	/** product id type argument */
	public static final String TYPE_ARGUMENT = "--type=";
	/** product id code argument */
	public static final String CODE_ARGUMENT = "--code=";
	/** product id source argument */
	public static final String SOURCE_ARGUMENT = "--source=";
	/** product id updateTime argument */
	public static final String UPDATE_TIME_ARGUMENT = "--updateTime=";

	/** product status argument */
	public static final String STATUS_ARGUMENT = "--status=";
	/** product delete argument */
	public static final String DELETE_ARGUMENT = "--delete";

	/** tracker url argument */
	public static final String TRACKER_URL_ARGUMENT = "--trackerURL=";

  /** property argument */
	public static final String PROPERTY_ARGUMENT = "--property-";
	/** eventID argument */
	public static final String EVENTID_ARGUMENT = "--eventid=";
	/** eventsource argument */
	public static final String EVENTSOURCE_ARGUMENT = "--eventsource=";
	/** eventsourcecode argument */
	public static final String EVENTSOURCECODE_ARGUMENT = "--eventsourcecode=";
	/** eventCode argument */
	public static final String EVENTCODE_ARGUMENT = "--eventcode=";
	/** eventtime argument */
	public static final String EVENTTIME_ARGUMENT = "--eventtime=";
	/** latitude argument */
	public static final String LATITUDE_ARGUMENT = "--latitude=";
	/** longitude argument */
	public static final String LONGITUDE_ARGUMENT = "--longitude=";
	/** depth argument */
	public static final String DEPTH_ARGUMENT = "--depth=";
	/** magnitude argument */
	public static final String MAGNITUDE_ARGUMENT = "--magnitude=";
	/** version argument */
	public static final String VERSION_ARGUMENT = "--version=";

	/** product link argument */
	public static final String LINK_ARGUMENT = "--link-";

	/** product content argument */
	public static final String CONTENT_ARGUMENT = "--content";
	/** product content type argument */
	public static final String CONTENT_TYPE_ARGUMENT = "--contentType=";
	/** product directory argument */
	public static final String DIRECTORY_ARGUMENT = "--directory=";
	/** product file argument */
	public static final String FILE_ARGUMENT = "--file=";

	/** private key argument */
	public static final String PRIVATE_KEY_ARGUMENT = "--privateKey=";
	/** signature version argument */
	public static final String SIGNATURE_VERSION_ARGUMENT = "--signatureVersion=";

	/** Property name used for configuring a tracker url. */
	public static final String TRACKER_URL_CONFIG_PROPERTY = "trackerURL";

	/** Property name used for configuring the list of senders. */
	public static final String SENDERS_CONFIG_PROPERTY = "senders";

	/** Arguments for configuring servers and connectTimeouts. */
	public static final String SERVERS_ARGUMENT = "--servers=";
	/** connectionTimeout argument */
	public static final String CONNECT_TIMEOUT_ARGUMENT = "--connectTimeout=";
	/** Default connectionTimeout argument. 15s */
	public static final Integer DEFAULT_CONNECT_TIMEOUT = 15000;
	/** binaryFormat argument */
	public static final String BINARY_FORMAT_ARGUMENT = "--binaryFormat";
	/** disableDeflate argument */
	public static final String DISABLE_DEFLATE = "--disableDeflate";
	/** disableParallelSend argument */
	public static final String DISABLE_PARALLEL_SEND = "--disableParallelSend";
	/** parallelSendTimeout argument */
	public static final String PARALLEL_SEND_TIMEOUT_ARGUMENT = "--parallelSendTimeout=";

	/** Tracker URL that is used when not overriden by an argument. */
	private URL defaultTrackerURL;

	/** ProductSenders that send the product after it is built. */
	private List<ProductSender> senders = new LinkedList<ProductSender>();

	/** The command line arguments being parsed. */
	private String[] args;

	private Integer connectTimeout = DEFAULT_CONNECT_TIMEOUT;
	private boolean parallelSend = Boolean.valueOf(ProductBuilder.DEFAULT_PARALLEL_SEND);
	private long parallelSendTimeout = Long.valueOf(ProductBuilder.DEFAULT_PARALLEL_SEND_TIMEOUT);

	/**
	 * This class is not intended to be instantiated directly.
	 * @param args arguments
	 */
	protected CLIProductBuilder(final String[] args) {
		this.args = args;
	}

	/**
	 * @return the senders
	 */
	public List<ProductSender> getSenders() {
		return senders;
	}

	/**
	 * @return the defaultTrackerURL
	 */
	public URL getDefaultTrackerURL() {
		return defaultTrackerURL;
	}

	/**
	 * @param defaultTrackerURL
	 *            the defaultTrackerURL to set
	 */
	public void setDefaultTrackerURL(URL defaultTrackerURL) {
		this.defaultTrackerURL = defaultTrackerURL;
	}

	/**
	 * Load ProductSenders that will send any built Products.
	 *
	 * There should be a property "senders" containing a comma delimited list of
	 * sender names to be loaded.
	 *
	 * @param config
	 *            the Config to load.
	 */
	public void configure(final Config config) throws Exception {
		Iterator<String> iter = StringUtils.split(
				config.getProperty(SENDERS_CONFIG_PROPERTY), ",").iterator();
		while (iter.hasNext()) {
			String senderName = iter.next();

			LOGGER.config("Loading sender '" + senderName + "'");
			// names reference global configuration objects.

			ProductSender sender = (ProductSender) Config.getConfig()
					.getObject(senderName);

			if (sender == null) {
				throw new ConfigurationException("Sender '" + senderName
						+ "' is not properly configured");
			}

			senders.add(sender);
		}

		String trackerURLProperty = config
				.getProperty(TRACKER_URL_CONFIG_PROPERTY);
		if (trackerURLProperty != null) {
			defaultTrackerURL = new URL(trackerURLProperty);
		}

		parallelSend = Boolean.valueOf(config.getProperty(
				ProductBuilder.PARALLEL_SEND_PROPERTY,
				ProductBuilder.DEFAULT_PARALLEL_SEND));
		parallelSendTimeout = Long.valueOf(config.getProperty(
				ProductBuilder.PARALLEL_SEND_TIMEOUT_PROPERTY,
				ProductBuilder.DEFAULT_PARALLEL_SEND_TIMEOUT));
		LOGGER.config("[" + getName() + "] parallel send enabled="
				+ parallelSend + ", timeout=" + parallelSendTimeout);
	}

	/**
	 * Called when the client is shutting down.
	 */
	public void shutdown() throws Exception {
		Iterator<ProductSender> iter = senders.iterator();
		while (iter.hasNext()) {
			try {
				iter.next().shutdown();
			} catch (Exception e) {
				LOGGER.log(Level.WARNING, "Exception shutting down sender", e);
			}
		}
	}

	/**
	 * Called when the client is done configuring.
	 */
	public void startup() throws Exception {
		Iterator<ProductSender> iter = senders.iterator();
		while (iter.hasNext()) {
			iter.next().startup();
		}
	}

	/**
	 * Send a product to all configured ProductSenders.
	 *
	 * @param product
	 *            the product to send.
	 * @return exceptions that occured while sending. If map is empty, there
	 *         were no exceptions.
	 */
	public Map<ProductSender, Exception> sendProduct(final Product product) {
		Map<ProductSender, Exception> sendExceptions = new HashMap<ProductSender, Exception>();

		Iterator<ProductSender> iter = senders.iterator();
		while (iter.hasNext()) {
			ProductSender sender = iter.next();
			try {
				sender.sendProduct(product);
			} catch (Exception e) {
				sendExceptions.put(sender, e);
			}
		}

		return sendExceptions;
	}

	/**
	 * Build a product using command line arguments.
	 *
	 * @return Product
	 * @throws Exception if error occurs
	 */
	public Product buildProduct() throws Exception {
		// start product id with null values, and verify they are all set after
		// all arguments are parsed.
		Product product = new Product(new ProductId(null, null, null));
		product.setTrackerURL(defaultTrackerURL);

		// These things are also processed after all arguments are parsed.
		// used with inline content
		boolean hasStdinContent = false;
		String contentType = null;
		// used when signing products
		File privateKey = null;
		Version signatureVersion = Version.SIGNATURE_V1;
		boolean binaryFormat = false;
		boolean enableDeflate = true;

		for (String arg : args) {
			if (arg.startsWith(TYPE_ARGUMENT)) {
				product.getId().setType(arg.replace(TYPE_ARGUMENT, ""));
			} else if (arg.startsWith(CODE_ARGUMENT)) {
				product.getId().setCode(arg.replace(CODE_ARGUMENT, ""));
			} else if (arg.startsWith(SOURCE_ARGUMENT)) {
				product.getId().setSource(arg.replace(SOURCE_ARGUMENT, ""));
			} else if (arg.startsWith(UPDATE_TIME_ARGUMENT)) {
				product.getId()
						.setUpdateTime(
								XmlUtils.getDate(arg.replace(
										UPDATE_TIME_ARGUMENT, "")));
			} else if (arg.startsWith(STATUS_ARGUMENT)) {
				product.setStatus(arg.replace(STATUS_ARGUMENT, ""));
			} else if (arg.equals(DELETE_ARGUMENT)) {
				product.setStatus(Product.STATUS_DELETE);
			} else if (arg.startsWith(TRACKER_URL_ARGUMENT)) {
				product.setTrackerURL(new URL(arg.replace(TRACKER_URL_ARGUMENT,
						"")));
			} else if (arg.startsWith(PROPERTY_ARGUMENT)) {
				String[] props = arg.replace(PROPERTY_ARGUMENT, "").split("=",
						2);
				try {
					product.getProperties().put(props[0], props[1]);
				} catch (IndexOutOfBoundsException ioobe) {
					throw new IllegalArgumentException(
							"Invalid property argument, must have value");
				}
			} else if (arg.startsWith(EVENTID_ARGUMENT)) {
				String id = arg.replace(EVENTID_ARGUMENT, "").toLowerCase();
				String eventNetwork = id.substring(0, 2);
				String eventNetworkId = id.substring(2);
				product.setEventId(eventNetwork, eventNetworkId);
			} else if (arg.startsWith(EVENTSOURCE_ARGUMENT)) {
				product.setEventSource(arg.replace(EVENTSOURCE_ARGUMENT, "")
						.toLowerCase());
			} else if (arg.startsWith(EVENTSOURCECODE_ARGUMENT)) {
				product.setEventSourceCode(arg.replace(
						EVENTSOURCECODE_ARGUMENT, "").toLowerCase());
			} else if (arg.startsWith(EVENTCODE_ARGUMENT)) {
				product.setEventSourceCode(arg.replace(EVENTCODE_ARGUMENT, "")
						.toLowerCase());
			} else if (arg.startsWith(EVENTTIME_ARGUMENT)) {
				product.setEventTime(XmlUtils.getDate(arg.replace(
						EVENTTIME_ARGUMENT, "")));
			} else if (arg.startsWith(MAGNITUDE_ARGUMENT)) {
				product.setMagnitude(new BigDecimal(arg.replace(
						MAGNITUDE_ARGUMENT, "")));
			} else if (arg.startsWith(LATITUDE_ARGUMENT)) {
				product.setLatitude(new BigDecimal(arg.replace(
						LATITUDE_ARGUMENT, "")));
			} else if (arg.startsWith(LONGITUDE_ARGUMENT)) {
				product.setLongitude(new BigDecimal(arg.replace(
						LONGITUDE_ARGUMENT, "")));
			} else if (arg.startsWith(DEPTH_ARGUMENT)) {
				product.setDepth(new BigDecimal(arg.replace(DEPTH_ARGUMENT, "")));
			} else if (arg.startsWith(VERSION_ARGUMENT)) {
				product.setVersion(arg.replace(VERSION_ARGUMENT, ""));
			} else if (arg.startsWith(LINK_ARGUMENT)) {
				String[] props = arg.replace(LINK_ARGUMENT, "").split("=", 2);
				try {
					product.addLink(props[0], new URI(props[1]));
				} catch (IndexOutOfBoundsException ioobe) {
					throw new IllegalArgumentException(
							"Invalid link, must have URL as value");
				}
			} else if (arg.equals(CONTENT_ARGUMENT)) {
				hasStdinContent = true;
			} else if (arg.startsWith(CONTENT_TYPE_ARGUMENT)) {
				contentType = arg.replace(CONTENT_TYPE_ARGUMENT, "");
			} else if (arg.startsWith(DIRECTORY_ARGUMENT)) {
				product.getContents().putAll(
						FileContent.getDirectoryContents(new File(arg.replace(
								DIRECTORY_ARGUMENT, ""))));
			} else if (arg.startsWith(FILE_ARGUMENT)) {
				File file = new File(arg.replace(FILE_ARGUMENT, ""));
				product.getContents()
						.put(file.getName(), new FileContent(file));
			} else if (arg.startsWith(PRIVATE_KEY_ARGUMENT)) {
				privateKey = new File(arg.replace(PRIVATE_KEY_ARGUMENT, ""));
			} else if (arg.startsWith(SIGNATURE_VERSION_ARGUMENT)) {
				signatureVersion = Version.fromString(
						arg.replace(SIGNATURE_VERSION_ARGUMENT, ""));
			} else if (arg.startsWith(SERVERS_ARGUMENT)) {
				senders.clear();
				senders.addAll(parseServers(arg.replace(SERVERS_ARGUMENT, ""),
						connectTimeout, binaryFormat, enableDeflate));
			} else if (arg.startsWith(CONNECT_TIMEOUT_ARGUMENT)) {
				connectTimeout = Integer.valueOf(arg.replace(
						CONNECT_TIMEOUT_ARGUMENT, ""));
			} else if (arg.equals(BINARY_FORMAT_ARGUMENT)) {
				binaryFormat = true;
			} else if (arg.equals(DISABLE_DEFLATE)) {
				enableDeflate = false;
			} else if (arg.equals(DISABLE_PARALLEL_SEND)) {
				parallelSend = false;
			} else if (arg.startsWith(PARALLEL_SEND_TIMEOUT_ARGUMENT)) {
				parallelSendTimeout = Long.valueOf(
						arg.replace(PARALLEL_SEND_TIMEOUT_ARGUMENT, ""));
			} else {
				// not a builder argument
			}
		}

		// validate product
		ProductId id = product.getId();
		if (id.getType() == null || id.getSource() == null
				|| id.getCode() == null || id.getUpdateTime() == null) {
			throw new IllegalArgumentException("Incomplete ProductId: source="
					+ id.getSource() + ", type=" + id.getType() + ", code="
					+ id.getCode() + ", updateTime=" + id.getUpdateTime());
		}

		// tracker url is required
		if (product.getTrackerURL() == null) {
			throw new IllegalArgumentException("Tracker URL is required");
		}

		if (hasStdinContent) {
			LOGGER.info("Reading content on standard input");

			ByteContent stdinContent = new ByteContent(new InputStreamContent(
					System.in));
			if (contentType != null) {
				stdinContent.setContentType(contentType);
			}
			product.getContents().put("", stdinContent);
		}

		// products that aren't being deleted should have content
		if (product.getContents().size() == 0 && !product.isDeleted()) {
			LOGGER.warning("Product has no content, are you sure this is intended?");
		}

		// mark which version of client was used to create product
		product.getProperties().put(ProductClient.PDL_CLIENT_VERSION_PROPERTY,
				ProductClient.RELEASE_VERSION);

		if (privateKey != null) {
			LOGGER.fine("Signing product");
			product.sign(
					CryptoUtils.readOpenSSHPrivateKey(
							StreamUtils.readStream(StreamUtils.getInputStream(privateKey)),
							null),
					signatureVersion);
		}

		return product;
	}

	/**
	 * Parse servers for list of product senders
	 * @param servers CSV string of servers
	 * @param connectTimeout timeout
	 * @param binaryFormat if binaryFormat
	 * @param enableDeflate if enableDeflate
	 * @return List of product senders
	 * */
	public static List<ProductSender> parseServers(final String servers,
			final Integer connectTimeout, final boolean binaryFormat,
			final boolean enableDeflate) throws Exception {
		List<ProductSender> senders = new ArrayList<ProductSender>();

		Iterator<String> iter = StringUtils.split(servers, ",").iterator();
		while (iter.hasNext()) {
			String server = iter.next();
			if (server.startsWith("https://")) {
				AwsProductSender sender = new AwsProductSender(new URL(server));
				senders.add(sender);
			} else {
				String[] parts = server.split(":");
				SocketProductSender sender = new SocketProductSender(parts[0],
						Integer.parseInt(parts[1]), connectTimeout);
				sender.setBinaryFormat(binaryFormat);
				sender.setEnableDeflate(enableDeflate);
				senders.add(sender);
			}
		}

		return senders;
	}

	/**
	 * Entry point into CLIProductBuilder.
	 *
	 * Called by Main if the --build argument is present.
	 *
	 * @param args arguments
	 * @throws Exception if error occurs
	 */
	public static void main(final String[] args) throws Exception {
		CLIProductBuilder builder = new CLIProductBuilder(args);
		builder.configure(Config.getConfig());
		builder.startup();

		Product product = null;
		try {
			product = builder.buildProduct();
		} catch (Exception e) {
			if (e.getMessage() == null) {
				LOGGER.log(Level.SEVERE, "Error building product", e);
			} else {
				LOGGER.severe("Invalid arguments: " + e.getMessage());
			}
			System.exit(EXIT_INVALID_ARGUMENTS);
		}

		if (product == null) {
			LOGGER.severe("Unable to build product");
			System.exit(EXIT_UNABLE_TO_BUILD);
		}

		// send tracker update
		new ProductTracker(product.getTrackerURL()).productCreated(
				SocketProductSender.class.getName(), product.getId());

		// send the product
		Map<ProductSender, Exception> sendExceptions = builder.parallelSend
				? ProductBuilder.parallelSendProduct(
							builder.senders,
							product,
							builder.parallelSendTimeout)
				: builder.sendProduct(product);

		// handle any send exceptions
		if (sendExceptions.size() != 0) {
			Iterator<ProductSender> senders = sendExceptions.keySet()
					.iterator();
			// log the exceptions
			while (senders.hasNext()) {
				ProductSender sender = senders.next();
				if (sender instanceof SocketProductSender) {
					// put more specific information about socket senders
					SocketProductSender socketSender = (SocketProductSender) sender;
					LOGGER.log(
							Level.WARNING,
							"Exception sending product to "
									+ socketSender.getHost() + ":"
									+ socketSender.getPort(),
							sendExceptions.get(sender));
				} else {
					LOGGER.log(Level.WARNING, "Exception sending product "
							+ sendExceptions.get(sender));
				}
			}

			if (sendExceptions.size() < builder.getSenders().size()) {
				LOGGER.warning("Partial failure sending product,"
						+ " at least one sender accepted product."
						+ " Check the tracker for more information.");
				// still output built product id
				System.out.println(product.getId().toString());
				// but exit with partial failure
				System.exit(EXIT_PARTIALLY_SENT);
			} else {
				LOGGER.severe("Total failure sending product");
				System.exit(EXIT_UNABLE_TO_SEND);
			}

		}

		// otherwise output built product id
		System.out.println(product.getId().toString());

		// normal exit
		builder.shutdown();
		System.exit(0);
	}
	/**
	 * Function on how to use command
	 * @return string
	 */
	public static String getUsage() {
		StringBuffer buf = new StringBuffer();

		buf.append("Product identification\n");
		buf.append("--source=SOURCE          product source, e.g. us, nc\n");
		buf.append("--type=TYPE              product type, e.g. shakemap, pager\n");
		buf.append("--code=CODE              product code, e.g. us2009abcd, nc12345678\n");
		buf.append("[--updateTime=TIME]      when the product was updated\n");
		buf.append("                         e.g. 2010-02-11T15:16:17+0000\n");
		buf.append("                         default is now\n");
		buf.append("[--status=STATUS]        product status\n");
		buf.append("                         default is UPDATE\n");
		buf.append("[--delete]               same as --status=DELETE\n");
		buf.append("\n");

		buf.append("Product contents\n");
		buf.append("[--directory=DIR]        read content from a directory, preserves hierarchy\n");
		buf.append("[--file=FILE]            read content from a file, added at top level of product\n");
		buf.append("[--content]              read content from STDIN\n");
		buf.append("[--contentType=MIMETYPE] used with --content to specify STDIN mime type\n");
		buf.append("\n");

		buf.append("Product metadata\n");
		buf.append("[--link-RELATION=URI]    link to another product or resource\n");
		buf.append("[--property-NAME=VALUE]  attributes of this product\n");
		buf.append("[--latitude=LAT]         Latitude of associated event.\n");
		buf.append("                             Decimal degrees\n");
		buf.append("                             Same as --property-latitude=LAT\n");
		buf.append("[--longitude=LNG]        Longitude of associated event.\n");
		buf.append("                             Decimal degrees\n");
		buf.append("                             Same as --property-longitude=LNG\n");
		buf.append("[--eventtime=TIME]       Time of associated event.\n");
		buf.append("                             Example: 2010-02-11T15:16:17+0000\n");
		buf.append("                             Same as --property-eventtime=TIME\n");
		buf.append("[--magnitude=MAG]        Magnitude of associated event.\n");
		buf.append("                             Same as --property-magnitude=MAG\n");
		buf.append("[--depth=DEPTH]          Depth of associated event.\n");
		buf.append("                             Kilometers.\n");
		buf.append("                             Same as --property-depth=DEPTH\n");
		buf.append("[--eventsource=SOURCE]   Network of associated event.\n");
		buf.append("                             Examples: us, nc, ci\n");
		buf.append("                             Same as --property-eventsource=SOURCE\n");
		buf.append("[--eventcode=CODE]       NetworkID of associated event.\n");
		buf.append("                             Examples: 2010abcd, 12345678\n");
		buf.append("                             Same as --property-eventsourcecode=CODE\n");
		buf.append("[--eventid=EVENTID]      Deprecated, use --eventsource and --eventsourcecode.\n");
		buf.append("                             Assumes a 10 character eventid: \n");
		buf.append("                                 assigns first 2 characters as 'eventsource',\n");
		buf.append("                                 rest as 'eventsourcecode'\n");
		buf.append("[--version=VERSION]      Internal product version.\n");
		buf.append("                             Same as --property-version=VERSION\n");
		buf.append("\n");

		buf.append("[--trackerURL=URL]       tracker url\n");
		buf.append("                         Override a configured default trackerURL\n");
		buf.append("[--privateKey=FILE]      OpenSSH DSA private key used to sign products\n");
		buf.append("                         A product signature may or may not be optional\n");
		buf.append("[--signatureVersion=v1]  signature version\n");
		buf.append("                         valid values are 'v1' or 'v2'\n");
		buf.append("\n");

		buf.append("Where product is sent\n");
		buf.append("[--connectTimeout=15000] Connect timeout in milliseconds\n");
		buf.append("                         Only used with --servers argument\n");
		buf.append("                         Must appear before --servers argument.\n");
		buf.append("[--binaryFormat]         Send to hub using binary format.\n");
		buf.append("                         Only used with --servers argument\n");
		buf.append("                         Must appear before --servers argument.\n");
		buf.append("[--disableDeflate]       Send to hub without using deflate compression.\n");
		buf.append("                         Only used with --servers argument\n");
		buf.append("                         Must appear before --servers argument.\n");
		buf.append("[--disableParallelSend]  Send to servers sequentially.\n");
		buf.append("[--parallelSendTimeout=300]\n");
		buf.append("                         timeout for parallel send in seconds.\n");
		buf.append("[--servers=SERVERLIST]   server:port[,server:port]\n");
		buf.append("                         Overrides any configured senders\n");
		buf.append("                         Example: pdldevel.cr.usgs.gov:11235\n");
		buf.append("\n");

		return buf.toString();
	}
}