Java tutorial
/* * Source code in 3rd-party is licensed and owned by their respective * copyright holders. * * All other source code is copyright Tresys Technology and licensed as below. * * Copyright (c) 2012 Tresys Technology LLC, Columbia, Maryland, USA * * This software was developed by Tresys Technology LLC * with U.S. Government sponsorship. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tresys.jalop.utils.jnltest.Config; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.InetAddress; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.beepcore.beep.profile.ProfileConfiguration; import org.beepcore.beep.profile.tls.jsse.TLSProfileJSSE; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import com.google.common.net.InetAddresses; import com.tresys.jalop.jnl.RecordType; import com.tresys.jalop.jnl.Role; /** * The {@link Config} class is used to to parse a configuration file, and * configure the JNLTest program. */ public class Config { private static final String ADDRESS = "address"; private static final String AUDIT = "audit"; private static final String BEEP_ACTION = "beepAction"; private static final String CONNECT = "connect"; private static final String DATA_CLASS = "dataClass"; private static final String HOSTS = "hosts"; private static final String INPUT = "input"; private static final String JOURNAL = "journal"; private static final String LISTENER = "listener"; private static final String LOG = "log"; private static final String OUTPUT = "output"; private static final String PEERS = "peers"; private static final String PENDING_DGST_MAX = "pendingDigestMax"; private static final String PENDING_DGST_TIMEOUT = "pendingDigestTimeout"; private static final String PORT = "port"; private static final String PUBLISH_ALLOW = "publishAllow"; private static final String PUBLISHER = "publisher"; private static final String SESSION_TIMEOUT = "sessionTimeout"; private static final String SUBSCRIBE_ALLOW = "subscribeAllow"; private static final String SUBSCRIBER = "subscriber"; /** * Method to create a {@link Config} from a {@link JSONObject} * * @param cfgFile * An identifier for the configuration file (i.e. the path). * @param parsedConfig * The root level {@link JSONObject} for the configuration file. * @return A {@link Config} object. * @throws ConfigurationException */ public static Config createFromJson(final String cfgFile, final JSONObject parsedConfig) throws ConfigurationException { final Config config = new Config(cfgFile); config.handleCommon(parsedConfig); final Object subscriber = parsedConfig.get(SUBSCRIBER); final Object publisher = parsedConfig.get(PUBLISHER); final Object listener = parsedConfig.get(LISTENER); final String exceptionMsg = new StringBuilder().append("Must specify one of '").append(SUBSCRIBER) .append("', '").append(PUBLISHER).append("', ").append(LISTENER).append('\'').toString(); if (subscriber != null) { if (publisher != null || listener != null) { throw new ConfigurationException(cfgFile, exceptionMsg); } config.handleSubscriber(asJsonObject(cfgFile, SUBSCRIBER, subscriber)); } else if (publisher != null) { if (listener != null) { throw new ConfigurationException(cfgFile, exceptionMsg); } config.handlePublisher(asJsonObject(cfgFile, PUBLISHER, publisher)); } else if (listener != null) { config.handleListener(asJsonObject(cfgFile, LISTENER, listener)); } else { throw new ConfigurationException(cfgFile, exceptionMsg); } return config; } /** * Parses a configuration file for use by the JNLTest program. * @param path * The path to a file to use as the configuration. * @return The {@link Config} * @throws ParseException * If there is a problem parsing the config file. * @throws IOException * If there is a problem reading the config file. * @throws ConfigurationException */ public static Config parse(final String path) throws IOException, ParseException, ConfigurationException { final FileReader fr = new FileReader(new File(path)); final JSONParser jsonParser = new JSONParser(); final Object o = jsonParser.parse(fr); JSONObject parsedConfig; parsedConfig = asJsonObject(path, null, o); return createFromJson(path, parsedConfig); } private InetAddress address; private boolean listener; private File inputPath; private File outputPath; private Map<InetAddress, PeerConfig> peerConfigs; private int pendingDigestMax; private int pendingDigestTimeout; private int port; private final Set<RecordType> recordTypes; private Role role; private Date sessionTimeout; private final String source; private ProfileConfiguration sslConfig; /** * Create a new {@link Config} object. * * @param source * This string will be used in generated errors to indicate what * resources caused a problem. */ Config(final String source) { this.source = source; this.recordTypes = new HashSet<RecordType>(); this.peerConfigs = new HashMap<InetAddress, PeerConfig>(); this.pendingDigestMax = -1; this.pendingDigestTimeout = -1; this.port = -1; } /** * Get the IP address. * * @return The {@link InetAddress}. Currently on IPv4 addresses are * supported. */ public InetAddress getAddress() { return this.address; } /** * Retrieve the directory path to use when acting as publisher. * * @return The directory specified for where to obtain records from. */ public File getInputPath() { return this.inputPath; } /** * Retrieve the directory path to use when acting as a subscriber. * * @return The directory specified for store records into. */ public File getOutputPath() { return this.outputPath; } /** * Obtains the {@link PeerConfig} map for this configuration. * * @return The a {@link Map} of {@link PeerConfig} objects. */ public Map<InetAddress, PeerConfig> getPeerConfigs() { return this.peerConfigs; } /** * Obtain the indicated maximum number of digests to calculate before * sending a "digest" message. This maximum is a per session maximum. * * @return The maximum number of digests to calculate before sending a * "digest" message. */ public int getPendingDigestMax() { return this.pendingDigestMax; } /** * Obtain the indicated number of seconds to wait before sending a "digest" * message. * * @return The maximum number of seconds to wait before sending a digest * message. */ public int getPendingDigestTimeout() { return this.pendingDigestTimeout; } /** * Get the port to listen on (for listeners) or connect to (for connector). * * @return The port * @see Config#isListener() */ public int getPort() { return this.port; } /** * Obtain the set of record types to subscribe/publish. This is not * applicable for a listener. * * @return The JALoP records that should be transfered using this * connection. * @see Config#isListener() */ public Set<RecordType> getRecordTypes() { return this.recordTypes; } /** * Obtain the indicated role, {@link Role#Publisher} or * {@link Role#Subscriber}. This is not applicable for listeners. * * @see Config#isListener() * @return The designated role. */ public Role getRole() { return this.role; } /** * Obtain the session timeout. The session timeout indicates how long an * connector should wait (once the connection is established) before * disconnecting from a remote. * * @return A Date that represents the amount of time to wait. This is the * amount of time to wait, not a future date. */ public Date getSessionTimeout() { return this.sessionTimeout; } /** * Obtain the identifier (i.e. path) of the configuration used to create * this {@link Config} object. * * @see Config#Config(String) * @return The source identifier. */ public String getSource() { return this.source; } /** * Get IP address from the {@link JSONObject}. This expects there to be a * key with the name "address" in the {@link JSONObject} obj. * * @param obj * The context to look in. * @throws ConfigurationException * If 'address' is not found. */ void handleAddress(final JSONObject obj) throws ConfigurationException { final String addrString = itemAsString(ADDRESS, obj); this.address = InetAddresses.forString(addrString); } /** * Helper utility to handle the common configuration keys. * * @param obj * The context to lookup keys in. * @throws ConfigurationException * If an error is detected in the configuration. */ void handleCommon(final JSONObject obj) throws ConfigurationException { handleAddress(obj); setPort(itemAsNumber(PORT, obj).intValue()); obj.get("ssl"); JSONObject ssl = asJsonObject(this.source, "ssl", obj.get("ssl"), false); if (ssl != null) { handleSslConfig(ssl); } } /** * Retrieve the {@link ProfileConfiguration}, if any, for setting up SSL. * The object returned by this function should be passed to the * {@link TLSProfileJSSE#init(String, ProfileConfiguration)} function to * finish the SSL configuration. * @return The {@link ProfileConfiguration} for SSL. */ public ProfileConfiguration getSslConfiguration() { return this.sslConfig; } /** * Handle common configuration keys for connectors. * * @param obj * The context to look up keys in. * @throws ConfigurationException * If an error is detected in the configuration. */ void handleSessionTimeout(final JSONObject obj) throws ConfigurationException { final String sessionTimeout = itemAsString(SESSION_TIMEOUT, obj); try { setSessionTimeout(new SimpleDateFormat("hh:mm:ss").parse(sessionTimeout)); } catch (final java.text.ParseException e) { throw new ConfigurationException(this.source, "Bad format for '" + SESSION_TIMEOUT + "' (" + sessionTimeout + ") " + e.toString()); } } /** * Handle parsing the dataClass field for a publisher or listener. * @param obj * The context to look up keys in. * @throws ConfigurationException * If an error is detected in the configuration. */ void handleDataClass(final JSONObject obj) throws ConfigurationException { final JSONArray dataClasses = itemAsArray(DATA_CLASS, obj); for (final Object o : dataClasses) { this.recordTypes.add(objectToRecordType(o)); } } /** * Helper utility to process the remainder of a configuration as a * 'listener'. * * @param obj * The context to look up keys in. * @throws ConfigurationException * If an error is detected in the configuration. */ void handleListener(final JSONObject obj) throws ConfigurationException { this.listener = true; setPendingDigestMax(itemAsNumber(PENDING_DGST_MAX, obj).intValue()); setPendingDigestTimeout(itemAsNumber(PENDING_DGST_TIMEOUT, obj).intValue()); setInputPath(new File(itemAsString(INPUT, obj, true))); setOutputPath(new File(itemAsString(OUTPUT, obj, true))); final JSONArray peers = itemAsArray(PEERS, obj); for (final Object o : peers) { final JSONObject elm = asJsonObject(this.source, null, o); updateKnownHosts(elm); } } /** * Helper utility to process a segment of the configuration as a * 'publisher'. * * @param publisher * The context to look up keys in. * @throws ConfigurationException * If an error is detected in the configuration. */ void handlePublisher(final JSONObject publisher) throws ConfigurationException { this.listener = false; handleSessionTimeout(publisher); handleDataClass(publisher); setRole(Role.Publisher); setInputPath(new File(itemAsString(INPUT, publisher, true))); } /** * Helper utility to process a segment of the configuration as a * 'subscriber'. * * @param subscriber * The context to look up keys in. * @throws ConfigurationException * If an error is detected in the configuration. */ void handleSubscriber(final JSONObject subscriber) throws ConfigurationException { this.listener = false; setRole(Role.Subscriber); handleSessionTimeout(subscriber); handleDataClass(subscriber); setOutputPath(new File(itemAsString(OUTPUT, subscriber, true))); setPendingDigestMax(itemAsNumber(PENDING_DGST_MAX, subscriber).intValue()); setPendingDigestTimeout(itemAsNumber(PENDING_DGST_TIMEOUT, subscriber).intValue()); } /** * Returns whether or not this Config specifies a listener, or connector. * * @return <code>true</code> if the configuration indicates to a listener. * <code>false</code> if the configuration specifies a connection should be * initiated. */ public boolean isListener() { return this.listener; } /** * Lookup the required element named by 'key' in the {@link JSONObject} obj. * * * @see Config#itemAsArray(String, JSONObject, boolean) * @param key * The name of the element to look up. * @param obj * The {@link JSONObject} to find the key in. * @return The {@link JSONArray} for the key, or NULL if none exists. * @throws ConfigurationException */ JSONArray itemAsArray(final String key, final JSONObject obj) throws ConfigurationException { return itemAsArray(key, obj, true); } /** * Simple helper function to retrieve an element as a {@link JSONArray}. If * the element is not an array, this function throws an appropriate * {@link ConfigurationException}. * * @param key * The name of the element to look up. * @param obj * The {@link JSONObject} to find the key in. * @param required * If this is set to true, a {@link ConfigurationException} is * thrown if the key is not found. * @return The {@link JSONArray} for the key, or NULL if none exists. * @throws ConfigurationException */ JSONArray itemAsArray(final String key, final JSONObject obj, final boolean required) throws ConfigurationException { final Object o = obj.get(key); if (!required && (o == null)) { return (JSONArray) o; } return asJsonArray(this.source, key, o); } /** * Look up the required key as a Number in the {@link JSONObject} obj. * * @see Config#itemAsNumber(String, JSONObject, boolean) * * @param key * The key to look up. * @param obj * The {@link JSONObject} to find the key in. * @return The value of key as a {@link Number}. * @throws ConfigurationException */ Number itemAsNumber(final String key, final JSONObject obj) throws ConfigurationException { return itemAsNumber(key, obj, true); } /** * Simple helper function to retrieve an element as a {@link Number}. If the * element is not a number, this function throws an appropriate * {@link ConfigurationException}. * * @param key * The name of the element to look up. * @param obj * The {@link JSONObject} to find the key in. * @param required * If this is set to true, a {@link ConfigurationException} is * thrown if the key is not found. * @return The {@link JSONArray} for the key, or NULL if none exists. * @throws ConfigurationException */ Number itemAsNumber(final String key, final JSONObject obj, final boolean required) throws ConfigurationException { final Object o = obj.get(key); if (!required && (o == null)) { return (Number) o; } return asNumberValue(this.source, key, o); } /** * Look up the required key in the {@link JSONObject} obj. * * @param key * The key to look up. * @param obj * The {@link JSONObject} to find the key in. * @return The value for key. * @throws ConfigurationException * @see {@link Config#itemAsString(String, JSONObject, boolean)} */ String itemAsString(final String key, final JSONObject obj) throws ConfigurationException { return itemAsString(key, obj, true); } /** * Simple helper function to retrieve an element as a {@link String}. If the * element is not a string, this function throws an appropriate * {@link ConfigurationException}. * * @param key * The name of the element to look up. * @param obj * The {@link JSONObject} to find the key in. * @param required * If this is set to true, a {@link ConfigurationException} is * thrown if the key is not found. * @return The value for the key, or NULL if none exists. * @throws ConfigurationException */ String itemAsString(final String key, final JSONObject obj, final boolean required) throws ConfigurationException { final Object o = obj.get(key); if (!required && (o == null)) { return (String) o; } return asStringValue(this.source, key, o); } /** * Helper utility to create a {@link RecordType} from a JSON Object. * * @param o * The object to convert to a {@link RecordType}. This is * expected to be a value from a {@link JSONObject} or * {@link JSONArray}. * @return The {@link RecordType} * @throws ConfigurationException * If there is a problem converting to a {@link RecordType} */ RecordType objectToRecordType(final Object o) throws ConfigurationException { final String dataClass = asStringValue(this.source, null, o); if (JOURNAL.equals(dataClass)) { return RecordType.Journal; } else if (AUDIT.equals(dataClass)) { return RecordType.Audit; } else if (LOG.equals(dataClass)) { return RecordType.Log; } throw new ConfigurationException(this.source, "Expected '" + DATA_CLASS + "' to be one of '" + JOURNAL + "', '" + AUDIT + "', or '" + LOG + "' (found '" + dataClass + "'."); } /** * Take a JSON array of strings and convert it to a set of * {@link RecordType}. * * @param recordTypesArr * @return A {@link Set} containing all the {@link RecordType} indicated in * recordTypesArr. * @throws ConfigurationException * If there is an error converting the values to a * {@link RecordType}s. */ Set<RecordType> recordSetFromArray(final JSONArray recordTypesArr) throws ConfigurationException { final Set<RecordType> rtSet = new HashSet<RecordType>(); if (recordTypesArr != null) { for (final Object o : recordTypesArr) { final RecordType rt = objectToRecordType(o); rtSet.add(rt); } } return rtSet; } /** * Set the address for this {@link Config}. * * @param address * The address. */ public void setAddress(final InetAddress address) { this.address = address; } /** * Configure this to initiate a connection, or listen for connections. * * @param connect * if set to true, specifies that a connection should be * initiated. If set to false, specifies to listen for * connections. */ public void setConnect(final boolean connect) { this.listener = connect; } /** * Set the path to a directory to user for input. Not applicable to a * "Subscriber". * * @param inputPath * The path to get records from. */ public void setInputPath(final File inputPath) { this.inputPath = inputPath; } /** * Set the path to store records from remotes in. Not applicable to a * "Publisher". * * @param outputPath */ public void setOutputPath(final File outputPath) { this.outputPath = outputPath; } /** * Set the peer configurations. * * @param peerConfigs * a {@link Map} of {@link InetAddress} to {@link PeerConfig} * objects indicating what records each remote is allowed to * publish/subscribe to. */ public void setPeerConfigs(final Map<InetAddress, PeerConfig> peerConfigs) { this.peerConfigs = peerConfigs; } /** * Set the maximum number of digests to store before sending a "digest" * message. * * @param pendingDigestMax * The number of digests to calculate before sending a "digest" * message. */ public void setPendingDigestMax(final int pendingDigestMax) { this.pendingDigestMax = pendingDigestMax; } /** * Set the maximum number of seconds to wait before sending a "digest" * message. * * @param pendingDigestTimeout * The time to wait, in seconds. */ public void setPendingDigestTimeout(final int pendingDigestTimeout) { this.pendingDigestTimeout = pendingDigestTimeout; } /** * Set the port to connect to/listen on. * * @param port * The port number. */ public void setPort(final int port) { this.port = port; } /** * Set the role. This is only applicable for connectors (i.e. * {@link Config#isListener()} returns <code>false</code>). * * @param role * The role. */ public void setRole(final Role role) { this.role = role; } /** * Configure the session timeout. This is only applicable for connectors * (i.e. {@link Config#isListener()} returns <code>false</code>). * * @param sessionTimeout */ public void setSessionTimeout(final Date sessionTimeout) { this.sessionTimeout = sessionTimeout; } /** * Helper utility to append permissions to the {@link PeerConfig} map. * * @param elm * A "PeerConfig" object. * @throws ConfigurationException * If an problem is detected in the configuration file. */ void updateKnownHosts(final JSONObject elm) throws ConfigurationException { final JSONArray hosts = itemAsArray(HOSTS, elm, false); final JSONArray pubAllowArray = itemAsArray(PUBLISH_ALLOW, elm, false); final Set<RecordType> pubAllow = recordSetFromArray(pubAllowArray); final JSONArray subAllowArray = itemAsArray(SUBSCRIBE_ALLOW, elm, false); final Set<RecordType> subAllow = recordSetFromArray(subAllowArray); for (final Object o : hosts) { String host; host = asStringValue(this.source, null, o); final InetAddress hostAddress = InetAddresses.forString(host); PeerConfig pc; if (this.peerConfigs.containsKey(hostAddress)) { pc = this.peerConfigs.get(hostAddress); } else { pc = new PeerConfig(); this.peerConfigs.put(hostAddress, pc); } pc.getSubscribeAllow().addAll(subAllow); pc.getPublishAllow().addAll(pubAllow); } } /** * Build a structure suitable to pass into the beepcore framework for SSL. * @param ssl The JSON object that contains all the keys to configure SSL. * * These keys are passed directly to the * {@link TLSProfileJSSE#init(String, ProfileConfiguration)}, so any keys * recognized by that class are valid here. * * @see TLSProfileJSSE#init(String, ProfileConfiguration) */ @SuppressWarnings("rawtypes") // because the JSON map doesn't use generics void handleSslConfig(JSONObject ssl) { this.sslConfig = new ProfileConfiguration(); Iterator iter = ssl.entrySet().iterator(); while (iter.hasNext()) { Entry e = (Entry) iter.next(); this.sslConfig.setProperty(e.getKey().toString(), e.getValue().toString()); } } /** * Helper utility to cast a {@link Object} as a {@link String}. * * @param path * The file where the error occurred. * @param key * The key (if any) for <code>o</code>. * @param o * The {@link Object} to cast. * @return <code>o</code> cast as a {@link String}. * @throws ConfigurationException * if <code>o</code> is <code>null</code> or not a * {@link String}. */ static String asStringValue(final String path, final String key, final Object o) throws ConfigurationException { if (o instanceof String) { return (String) o; } if (key != null) { throw new ConfigurationException(path, "Expected String for '" + key + "', found '" + o + "'"); } else { throw new ConfigurationException(path, "Expected String, found '" + o + "'"); } } /** * Helper utility to cast a {@link Object} as a {@link Number}. * * @param path * The file where the error occurred. * @param key * The key (if any) for <code>o</code>. * @param o * The {@link Object} to cast. * @return <code>o</code> cast as a {@link Number}. * @throws ConfigurationException * if <code>o</code> is <code>null</code> or not a * {@link Number}. */ static Number asNumberValue(final String path, final String key, final Object o) throws ConfigurationException { if (o instanceof Number) { return (Number) o; } if (key != null) { throw new ConfigurationException(path, "Expected Number for '" + key + "', found '" + o + "'"); } else { throw new ConfigurationException(path, "Expected Number, found '" + o + "'"); } } /** * Helper utility to cast an {@link Object} as a {@link JSONObject}. * * @param path * The file where the error occurred. * @param key * The key (if any) for <code>o</code>. * @param o * The {@link Object} to cast * @return <code>o</code> cast as a {@link JSONObject}. * @throws ConfigurationException * if <code>o</code> is <code>null</code> or not a * {@link JSONObject}. */ static JSONObject asJsonObject(final String path, final String key, final Object o) throws ConfigurationException { return asJsonObject(path, key, o, true); } /** * Helper utility to cast an {@link Object} as a {@link JSONObject}. * * @param path * The file where the error occurred. * @param key * The key (if any) for <code>o</code>. * @param o * The {@link Object} to cast * @return <code>o</code> cast as a {@link JSONObject}. * @throws ConfigurationException * if <code>o</code> is <code>null</code> or not a * {@link JSONObject}. */ static JSONObject asJsonObject(String path, String key, Object o, boolean required) throws ConfigurationException { if (o instanceof JSONObject) { return (JSONObject) o; } if (key != null && o != null) { throw new ConfigurationException(path, "Expected JSON Object for '" + key + "', found '" + o + "'"); } else if (required) { throw new ConfigurationException(path, "Expected JSON Object, found '" + o + "'"); } return null; } /** * Helper utility to cast a value to a {@link JSONArray}. * * @param path * The file where the error occurred. * @param key * The key (if any) for <code>o</code>. * @param o * The {@link Object} to cast. * @return <code>o</code> cast as a {@link JSONArray} * @throws ConfigurationException * if <code>o</code> is <code>null</code> or not a * {@link JSONArray}. */ static JSONArray asJsonArray(final String path, final String key, final Object o) throws ConfigurationException { if (o instanceof JSONArray) { return (JSONArray) o; } if (key != null) { throw new ConfigurationException(path, "Expected JSON Array for '" + key + "', found '" + o + "'"); } else { throw new ConfigurationException(path, "Expected JSON Array, found '" + o + "'"); } } }