Java tutorial
/* * * Copyright (C) 2012-2014 R T Huitema. All Rights Reserved. * Web: www.42.co.nz * Email: robert@42.co.nz * Author: R T Huitema * * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. * * 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 nz.co.fortytwo.signalk.util; import static nz.co.fortytwo.signalk.util.SignalKConstants.CONFIG; import static nz.co.fortytwo.signalk.util.SignalKConstants.KNOTS_TO_MS; import static nz.co.fortytwo.signalk.util.SignalKConstants.LIST; import static nz.co.fortytwo.signalk.util.SignalKConstants.MS_TO_KNOTS; import static nz.co.fortytwo.signalk.util.SignalKConstants.dot; import static nz.co.fortytwo.signalk.util.SignalKConstants.resources; import static nz.co.fortytwo.signalk.util.SignalKConstants.self; import static nz.co.fortytwo.signalk.util.SignalKConstants.self_str; import static nz.co.fortytwo.signalk.util.SignalKConstants.sources; import static nz.co.fortytwo.signalk.util.SignalKConstants.timestamp; import static nz.co.fortytwo.signalk.util.SignalKConstants.version; import static nz.co.fortytwo.signalk.util.SignalKConstants.vessels; import static nz.co.fortytwo.signalk.util.SignalKConstants.vessels_dot_self; import static nz.co.fortytwo.signalk.util.SignalKConstants.vessels_dot_self_dot; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.StringReader; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.NavigableSet; import java.util.Properties; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import mjson.Json; import net.sf.marineapi.nmea.sentence.RMCSentence; import nz.co.fortytwo.signalk.model.SignalKModel; import nz.co.fortytwo.signalk.model.impl.SignalKModelFactory; /** * Place for all the left over bits that are used across Signalk * * @author robert * */ public class Util { private static Logger logger = LogManager.getLogger(Util.class); // private static Properties props; protected static SignalKModel model = null; public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_hh:mm:ss"); //public static File cfg = null; private static boolean timeSet = false; public static final double R = 6372800; // In meters protected static Pattern selfMatch = Pattern.compile("\\.self\\."); protected static String dot_self_dot = dot + self + dot; protected static Pattern selfEndMatch = Pattern.compile("\\.self$"); protected static String dot_self = dot + self; private static String rootPath = ""; /** * Set the root path * * @param rootPath */ public static void setRootPath(String rootPath) { Util.rootPath = rootPath; } /** * Get the root path * * @return */ public static String getRootPath() { return rootPath; } /** * Smooth the data a bit * * @param prev * @param current * @return */ public static double movingAverage(double ALPHA, double prev, double current) { prev = ALPHA * prev + (1 - ALPHA) * current; return prev; } /** * Load the config from the default location The config is cached, * subsequent calls get the same object * * @param dir * @return * @throws FileNotFoundException * @throws IOException */ public static void getConfig() throws FileNotFoundException, IOException { logger.info("getConfig()"); model = SignalKModelFactory.getInstance(); //Util.setDefaults(model); //SignalKModelFactory.loadConfig(model); //String mySelf = (String) model.get(ConfigConstants.UUID); //Util.setSelf(mySelf); } /** * Load the provided model as the config. The config is cached, subsequent * calls get the same object Useful in tests * * @param dir * @return * @throws FileNotFoundException * @throws IOException */ public static void setConfig(SignalKModel model) throws FileNotFoundException, IOException { Util.model = model; } /** * Config defaults * * @param props */ public static void setDefaults(SignalKModel model) { // populate sensible defaults here logger.info("setDefaults()"); model.getFullData().put(ConfigConstants.UUID, "self"); model.getFullData().put(ConfigConstants.WEBSOCKET_PORT, 3000); model.getFullData().put(ConfigConstants.REST_PORT, 8080); model.getFullData().put(ConfigConstants.STORAGE_ROOT, "./storage/"); model.getFullData().put(ConfigConstants.STATIC_DIR, "./signalk-static/"); model.getFullData().put(ConfigConstants.MAP_DIR, "./mapcache/"); model.getFullData().put(ConfigConstants.DEMO, false); model.getFullData().put(ConfigConstants.STREAM_URL, "motu.log"); model.getFullData().put(ConfigConstants.USBDRIVE, "/media/usb0"); model.getFullData().put(ConfigConstants.SERIAL_PORTS, "[\"/dev/ttyUSB0\",\"/dev/ttyUSB1\",\"/dev/ttyUSB2\",\"/dev/ttyACM0\",\"/dev/ttyACM1\",\"/dev/ttyACM2\"]"); if (SystemUtils.IS_OS_WINDOWS) { model.getFullData().put(ConfigConstants.SERIAL_PORTS, "[\"COM1\",\"COM2\",\"COM3\",\"COM4\"]"); } model.getFullData().put(ConfigConstants.SERIAL_PORT_BAUD, 38400); model.getFullData().put(ConfigConstants.ENABLE_SERIAL, true); model.getFullData().put(ConfigConstants.TCP_PORT, 55555); model.getFullData().put(ConfigConstants.UDP_PORT, 55554); model.getFullData().put(ConfigConstants.TCP_NMEA_PORT, 55557); model.getFullData().put(ConfigConstants.UDP_NMEA_PORT, 55556); model.getFullData().put(ConfigConstants.STOMP_PORT, 61613); model.getFullData().put(ConfigConstants.MQTT_PORT, 1883); model.getFullData().put(ConfigConstants.CLOCK_source, "system"); model.getFullData().put(ConfigConstants.HAWTIO_PORT, 8000); model.getFullData().put(ConfigConstants.HAWTIO_AUTHENTICATE, false); model.getFullData().put(ConfigConstants.HAWTIO_CONTEXT, "/hawtio"); model.getFullData().put(ConfigConstants.HAWTIO_WAR, "./hawtio/hawtio-default-offline-1.4.48.war"); model.getFullData().put(ConfigConstants.HAWTIO_START, false); model.getFullData().put(ConfigConstants.JOLOKIA_PORT, 8001); model.getFullData().put(ConfigConstants.JOLOKIA_AUTHENTICATE, false); model.getFullData().put(ConfigConstants.JOLOKIA_CONTEXT, "/jolokia"); model.getFullData().put(ConfigConstants.JOLOKIA_WAR, "./hawtio/jolokia-war-1.3.3.war"); model.getFullData().put(ConfigConstants.VERSION, "1.0.0"); model.getFullData().put(ConfigConstants.ALLOW_INSTALL, true); model.getFullData().put(ConfigConstants.ALLOW_UPGRADE, true); model.getFullData().put(ConfigConstants.GENERATE_NMEA0183, true); model.getFullData().put(ConfigConstants.ZEROCONF_AUTO, true); model.getFullData().put(ConfigConstants.START_MQTT, true); model.getFullData().put(ConfigConstants.START_STOMP, true); // Instrument ofsets, adjustments, display units model.getFullData().put(ConfigConstants.COMPASS_OFFSET, 10.); model.getFullData().put(ConfigConstants.SURFACE_TO_TRANSDUCER, 0.5); model.getFullData().put(ConfigConstants.TRANSDUCER_TO_KEEL, 1.5); model.getFullData().put(ConfigConstants.SOG_DISPLAY_UNIT, "Kt"); model.getFullData().put(ConfigConstants.STW_DISPLAY_UNIT, "Kt"); model.getFullData().put(ConfigConstants.DEPTH_USER_UNIT, "ft"); model.getFullData().put(ConfigConstants.DEPTH_ALARM_METHOD, "visual"); model.getFullData().put(ConfigConstants.DEPTH_WARN_METHOD, "visual"); model.getFullData().put(ConfigConstants.ENGINE_TEMP_USER_UNIT, "F"); model.getFullData().put(ConfigConstants.ENGINE_TEMP_ALARM_METHOD, "visual"); model.getFullData().put(ConfigConstants.ENGINE_TEMP_WARN_METHOD, "visual"); model.getFullData().put(ConfigConstants.DEPTH_SPARKLINE_POINTS, 200.); model.getFullData().put(ConfigConstants.DEPTH_SPARKLINE_MIN, 1.6); // construct the engine temperature zones Json alarmZone = Json.object("lower", "190", "upper", "250", "state", "alarm", "message", "Engine Overtemperature"); Json warnZone = Json.object("lower", "180", "upper", "190", "state", "warn", "message", "Engine Warm"); Json normalZone = Json.object("lower", "0", "upper", "180", "state", "normal", "message", ""); Json zones = Json.array(alarmZone, warnZone, normalZone); model.getFullData().put(ConfigConstants.ENGINE_TEMP_ALARM_ZONES, zones); alarmZone = Json.object("lower", "0.0", "upper", "6.0", "state", "alarm", "message", "Danger"); warnZone = Json.object("lower", "6.0", "upper", "9.0", "state", "warn", "message", "Shallow Water"); normalZone = Json.object("lower", "1.65", "upper", "9999", "state", "normal", "message", ""); zones = Json.array(alarmZone, warnZone, normalZone); model.getFullData().put(ConfigConstants.DEPTH_ALARM_ZONES, zones); // model.getFullData().put(ConfigConstants.WIND_OFFSET, 170); //control config, only local networks Json ips = Json.array(); Enumeration<NetworkInterface> interfaces; try { interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface i = interfaces.nextElement(); for (InterfaceAddress iAddress : i.getInterfaceAddresses()) { //ignore IPV6 for now. if (iAddress.getAddress().getAddress().length > 4) { continue; } ips.add(iAddress.getAddress().getHostAddress() + "/" + iAddress.getNetworkPrefixLength()); } } model.getFullData().put(ConfigConstants.SECURITY_CONFIG, ips.toString()); } catch (SocketException e) { logger.error(e.getMessage(), e); } //model.getFullData().put(ConfigConstants.CLIENT_WS, null); //model.getFullData().put(ConfigConstants.CLIENT_TCP, null); //model.getFullData().put(ConfigConstants.CLIENT_MQTT, null); //model.getFullData().put(ConfigConstants.CLIENT_STOMP, null); } public static Json getWelcomeMsg() { Json msg = Json.object(); msg.set(version, getVersion()); msg.set(timestamp, getIsoTimeString()); msg.set(self_str, getConfigProperty(ConfigConstants.UUID)); return msg; } public static String getVersion() { return getConfigProperty(ConfigConstants.VERSION); } /** * Round to specified decimals * * @param val * @param places * @return */ public static double round(double val, int places) { double scale = Math.pow(10, places); long iVal = Math.round(val * scale); return iVal / scale; } /** * Attempt to set the system time using the GPS time * * @param sen */ @SuppressWarnings("deprecation") public static void checkTime(RMCSentence sen) { if (timeSet) { return; } try { net.sf.marineapi.nmea.util.Date dayNow = sen.getDate(); // if we need to set the time, we will be WAAYYY out // we only try once, so we dont get lots of native processes // spawning if we fail timeSet = true; Date date = new Date(); if ((date.getYear() + 1900) == dayNow.getYear()) { if (logger.isDebugEnabled()) { logger.debug("Current date is " + date); } return; } // so we need to set the date and time net.sf.marineapi.nmea.util.Time timeNow = sen.getTime(); String yy = String.valueOf(dayNow.getYear()); String MM = pad(2, String.valueOf(dayNow.getMonth())); String dd = pad(2, String.valueOf(dayNow.getDay())); String hh = pad(2, String.valueOf(timeNow.getHour())); String mm = pad(2, String.valueOf(timeNow.getMinutes())); String ss = pad(2, String.valueOf(timeNow.getSeconds())); if (logger.isDebugEnabled()) { logger.debug("Setting current date to " + dayNow + " " + timeNow); } String cmd = "sudo date --utc " + MM + dd + hh + mm + yy + "." + ss; Runtime.getRuntime().exec(cmd.split(" "));// MMddhhmm[[yy]yy] if (logger.isDebugEnabled()) { logger.debug("Executed date setting command:" + cmd); } } catch (Exception e) { logger.error(e.getMessage(), e); } } /** * pad the value to i places, eg 2 >> 02 * * @param i * @param valueOf * @return */ private static String pad(int i, String value) { while (value.length() < i) { value = "0" + value; } return value; } /** * Convert a speed in knots to meters/sec * * @param speed in knots * @return speed in m/s */ public static double kntToMs(double speed) { return speed * KNOTS_TO_MS; } /** * Convert a speed in meter/sec to knots * * @param speed in m/s * @return speed in knots */ public static double msToKnts(double speed) { return speed * MS_TO_KNOTS; } /** * Convert a distance in fathoms to meters * * @param fathoms * @return distance in meters */ public static double fToM(double fathoms) { return fathoms / SignalKConstants.MTR_TO_FATHOM; } public static double cToFahr(double c) { return c * (9. / 5.) * c + 32.; } public static double fahrToC(double f) { return (f - 32.) * 5. / 9.; } /** * Convert a distance in ft to meters * * @param feet * @return distance in meters */ public static double ftToM(double feet) { return feet / SignalKConstants.MTR_TO_FEET; } public static String getConfigProperty(String prop) { try { return (String) model.get(prop); } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public static Json getConfigJsonArray(String prop) { try { String arrayStr = (String) model.get(prop); if (StringUtils.isNotBlank(arrayStr) && arrayStr.length() > 2) { Json array = Json.read(arrayStr); return array; } } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public static Integer getConfigPropertyInt(String prop) { try { if (model.get(prop) instanceof String) { return (Integer.valueOf((String) model.get(prop))); } if (model.get(prop) instanceof Number) { return ((Number) model.get(prop)).intValue(); } return (Integer) model.get(prop); } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public static Double getConfigPropertyDouble(String prop) { try { if (model.get(prop) instanceof String) { return (Double.valueOf((String) model.get(prop))); } return (Double) model.get(prop); } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public static Boolean getConfigPropertyBoolean(String prop) { try { if (model.get(prop) instanceof Boolean) { return ((Boolean) model.get(prop)); } return new Boolean((String) model.get(prop)); } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public static Pattern regexPath(String newPath) { // regex it String regex = newPath.replaceAll(".", "[$0]").replace("[*]", ".*").replace("[?]", "."); return Pattern.compile(regex); } public static String fixSelfKey(String key) { key = selfMatch.matcher(key).replaceAll(dot_self_dot); key = selfEndMatch.matcher(key).replaceAll(dot_self); return key; } public static String sanitizePath(String newPath) { newPath = newPath.replace('/', '.'); if (newPath.startsWith(dot)) { newPath = newPath.substring(1); } if (!newPath.endsWith("*") || !newPath.endsWith("?")) { newPath = newPath + "*"; } return newPath; } public static void populateTree(SignalKModel signalkModel, SignalKModel temp, String p) { NavigableSet<String> node = signalkModel.getTree(p); if (logger.isDebugEnabled()) { logger.debug("Found node:" + p + " = " + node); } if (node != null && node.size() > 0) { addNodeToTemp(signalkModel, temp, node); } else { try { temp.getFullData().put(p, signalkModel.get(p)); } catch (Exception e) { logger.error("Key: " + p + ", " + e.getMessage(), e); } } } public static SignalKModel populateModel(SignalKModel model, String mapDump) throws IOException { Properties props = new Properties(); props.load(new StringReader(mapDump.substring(1, mapDump.length() - 1).replace(", ", "\n"))); for (Map.Entry<Object, Object> e : props.entrySet()) { if (e.getValue().equals("true") || e.getValue().equals("false")) { model.getFullData().put((String) e.getKey(), Boolean.getBoolean((String) e.getValue())); } else if (NumberUtils.isNumber((String) e.getValue())) { model.getFullData().put((String) e.getKey(), NumberUtils.createDouble((String) e.getValue())); } else { model.getFullData().put((String) e.getKey(), e.getValue()); } } return model; } public static SignalKModel populateModel(SignalKModel signalk, File file) throws IOException { return populateModel(signalk, FileUtils.readFileToString(file)); } /** * Recursive findNode(). Returns null if not found * * @param node * @param fullPath * @return */ public static Json findNode(Json node, String fullPath) { String[] paths = fullPath.split("\\."); // Json endNode = null; for (String path : paths) { logger.debug("findNode:" + path); node = node.at(path); if (node == null) { return null; } } return node; } public static void addNodeToTemp(SignalKModel temp, NavigableSet<String> node) { SignalKModel model = SignalKModelFactory.getInstance(); addNodeToTemp(model, temp, node); } public static void addNodeToTemp(SignalKModel model, SignalKModel temp, NavigableSet<String> node) { for (String key : node) { temp.getFullData().put(key, model.get(key)); } } public static String getIsoTimeString() { return getIsoTimeString(System.currentTimeMillis()); // return ISO8601DateFormat.getDateInstance().format(new Date()); } public static String getIsoTimeString(DateTime now) { return now.toDateTimeISO().toString(); } public static String getIsoTimeString(long timestamp) { return new DateTime(timestamp, DateTimeZone.UTC).toDateTimeISO().toString(); } public static double haversineMeters(double lat, double lon, double anchorLat, double anchorLon) { double dLat = Math.toRadians(anchorLat - lat); double dLon = Math.toRadians(anchorLon - lon); lat = Math.toRadians(lat); anchorLat = Math.toRadians(anchorLat); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat) * Math.cos(anchorLat); double c = 2 * Math.asin(Math.sqrt(a)); return R * c; } public static String getContext(String path) { // return vessels.* // TODO; robustness for "signalk/api/v1/", and "vessels.*" and // "list/vessels" if (StringUtils.isBlank(path)) { return ""; } if (path.equals(resources) || path.startsWith(resources + dot)) { return path; } if (path.equals(sources) || path.startsWith(sources + dot)) { return path; } if (path.equals(CONFIG)) { return path; } if (path.startsWith(CONFIG + dot)) { int p1 = path.indexOf(CONFIG) + CONFIG.length() + 1; int pos = path.indexOf(".", p1); if (pos < 0) { return path; } return path.substring(0, pos); } if (path.equals(vessels)) { return path; } if (path.startsWith(vessels + dot) || path.startsWith(LIST + dot + vessels + dot)) { int p1 = path.indexOf(vessels) + vessels.length() + 1; int pos = path.indexOf(".", p1); if (pos < 0) { return path; } return path.substring(0, pos); } return ""; } public static void setSelf(String self) { //self = self; dot_self_dot = dot + self + dot; dot_self = dot + self; SignalKConstants.self = self; vessels_dot_self_dot = vessels + dot + self + dot; vessels_dot_self = vessels + dot + self; logger.info("Setting self:" + self); logger.info("Setting vessels.self:" + vessels_dot_self); } public static boolean sameNetwork(String localAddress, String remoteAddress) throws Exception { InetAddress addr = InetAddress.getByName(localAddress); NetworkInterface networkInterface = NetworkInterface.getByInetAddress(addr); short netmask = -1; for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) { if (address.getAddress().equals(addr)) { netmask = address.getNetworkPrefixLength(); } } return sameNetwork(localAddress, netmask, remoteAddress); } public static boolean sameNetwork(String localAddress, short netmask, String remoteAddress) throws Exception { byte[] a1 = InetAddress.getByName(localAddress).getAddress(); byte[] a2 = InetAddress.getByName(remoteAddress).getAddress(); byte[] m = InetAddress.getByName(normalizeFromCIDR(netmask)).getAddress(); if (logger.isDebugEnabled()) { logger.debug("sameNetwork?:" + localAddress + "/" + normalizeFromCIDR(netmask) + "," + remoteAddress + "," + netmask); } for (int i = 0; i < a1.length; i++) { if ((a1[i] & m[i]) != (a2[i] & m[i])) { return false; } } return true; } public static boolean inNetworkList(List<String> ipList, String ip) throws Exception { for (String denyIp : ipList) { short netmask = 0; String[] p = denyIp.split("/"); if (p.length > 1) { netmask = (short) (32 - Short.valueOf(p[1])); } if (Util.sameNetwork(p[0], netmask, ip)) { if (logger.isDebugEnabled()) { logger.debug("IP found " + ip + " in list: " + denyIp); } return true; } } return false; } /* * RFC 1518, 1519 - Classless Inter-Domain Routing (CIDR) * This converts from "prefix + prefix-length" format to * "address + mask" format, e.g. from xxx.xxx.xxx.xxx/yy * to xxx.xxx.xxx.xxx/yyy.yyy.yyy.yyy. */ public static String normalizeFromCIDR(short bits) { final int mask = (bits == 32) ? 0 : 0xFFFFFFFF - ((1 << bits) - 1); return Integer.toString(mask >> 24 & 0xFF, 10) + "." + Integer.toString(mask >> 16 & 0xFF, 10) + "." + Integer.toString(mask >> 8 & 0xFF, 10) + "." + Integer.toString(mask >> 0 & 0xFF, 10); } }