+/*
+ * The MIT License
+ *
+ * Copyright 2015 TJ <hacker@iamtj>.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package uk.ac.ntu.n0521366.wsyd.libs.net;
+
+import java.net.InetSocketAddress;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Maps service names on hosts to an InetAddress:Port pair.
+ *
+ * @author TJ <hacker@iamtj>
+ */
+public class ServiceAddressMap {
+ /**
+ * Multi-thread safe map keyed on the service name.
+ */
+ protected ConcurrentHashMap<String, LastSeenHost> _serviceToAddressMap = new ConcurrentHashMap<>();
+
+ /**
+ * Encapsulates a unique network host and the last time it was seen.
+ */
+ public static class LastSeenHost {
+ /**
+ * State of a record:
+ *
+ * STATIC = was manually entered and should not be removed
+ * DYNAMIC = was discovered via multicast announcement or new connection and can be removed
+ */
+ public static enum STATE {STATIC, DYNAMIC}
+
+ final STATE state;
+ final long timeInMillis;
+ final InetSocketAddress address;
+
+ /**
+ * Constructs a new instance of a host.
+ *
+ * @param address The current address:port
+ * @param timeInMillis time last seen
+ * @param state whether record was added through dynamic discovery
+ */
+ LastSeenHost(InetSocketAddress address, long timeInMillis, STATE state) {
+ this.address = address;
+ this.timeInMillis = timeInMillis;
+ this.state = state;
+ }
+
+ /**
+ * Construct a new instance of a dynamically announced host.
+ *
+ * @param host
+ */
+ LastSeenHost(InetSocketAddress host) {
+ this(host, System.currentTimeMillis(), STATE.DYNAMIC);
+ }
+
+ /**
+ * Formatted string representation of InetAddress and timestamp.
+ * @return the representation
+ */
+ @Override
+ public String toString() {
+ return MessageFormat.format("{0}:{1,number,integer}@{2}", this.address.getHostString(), this.address.getPort(), this.timeInMillis);
+ }
+ };
+
+ /**
+ * The Logger to use.
+ */
+ private static Logger LOGGER = null;
+
+ /**
+ * Facility name for logger
+ */
+ private String _facility = null;
+
+ /**
+ * Construct a map with a Logger.
+ *
+ * @param facility The log facility to tag messages with
+ * @param logger The Logger
+ */
+ public ServiceAddressMap(String facility, Logger logger) {
+ this._facility = facility;
+ LOGGER = logger;
+ }
+
+ /**
+ * Log some message.
+ * @param level
+ * @param message
+ */
+ private void log(Level level, String message) {
+ if (LOGGER == null)
+ return;
+ LOGGER.logp(level, _facility, null, message);
+
+ }
+ /**
+ * Remove stale service records.
+ *
+ * @param ageInMillis milliseconds since last seen to be considered stale
+ * @return quantity of records removed
+ */
+ public ArrayList<String> cleanServiceAddressMap(long ageInMillis) {
+ ArrayList<String> result = new ArrayList<>();
+ long expireTime = System.currentTimeMillis() - ageInMillis;
+ java.util.Enumeration<String> keys = this._serviceToAddressMap.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+
+ // XXX: special handling for "all" target - never remove it
+ LastSeenHost host = _serviceToAddressMap.get(key);
+ if (host != null && host.state == LastSeenHost.STATE.DYNAMIC) {
+ if (host.timeInMillis < expireTime) {
+ if (_serviceToAddressMap.remove(key, host)) {
+ result.add(key);
+ log(Level.INFO, MessageFormat.format("Removed \"{0}\" from service map", key));
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Ensure service is in the map of known hosts.
+ * @param service the service name to check
+ * @return true is the target service is known
+ */
+ protected boolean isServiceValid(String service) {
+ return this._serviceToAddressMap.containsKey(service);
+ }
+
+ /**
+ * Get the current InetSocketAddress of a target.
+ *
+ * @param target name of the service
+ * @return IP address and port of the service
+ */
+ public InetSocketAddress getServiceAddress(String target) {
+ InetSocketAddress result = null;
+
+ if (target != null && target.length() > 0) {
+ LastSeenHost host = this._serviceToAddressMap.get(target);
+ if (host != null)
+ result = host.address;
+ }
+
+ return result;
+ }
+
+ public void put(String service, LastSeenHost host) {
+ this._serviceToAddressMap.put(service, host);
+ }
+
+ public LastSeenHost get(String service) {
+ return this._serviceToAddressMap.get(service);
+ }
+}