From 0ebbb2dcbffa5e28ffd8f721fc06cb41c111e0b4 Mon Sep 17 00:00:00 2001 From: aleigh Date: Sun, 29 May 2022 19:25:00 -0700 Subject: [PATCH] ATG-84: Support zero configuration for historian --- .../src/main/java/lc/esp/sdk/ESPAddress.java | 23 +- .../main/java/lc/esp/sdk/ESPAddressClass.java | 14 +- .../src/main/java/lc/esp/sdk/ESPClient.java | 7 +- .../anthonynsimon/url/DefaultURLParser.java | 230 +++++++++ .../com/anthonynsimon/url/PathResolver.java | 109 +++++ .../com/anthonynsimon/url/PercentEncoder.java | 235 +++++++++ .../main/java/com/anthonynsimon/url/URL.java | 450 ++++++++++++++++++ .../com/anthonynsimon/url/URLBuilder.java | 65 +++ .../java/com/anthonynsimon/url/URLParser.java | 13 + .../java/com/anthonynsimon/url/URLPart.java | 13 + .../url/exceptions/InvalidHexException.java | 15 + .../InvalidURLReferenceException.java | 15 + .../url/exceptions/MalformedURLException.java | 15 + .../historian/service/HistorianService.java | 27 +- .../src/main/java/lc/zero/sdk/ZeroClient.java | 16 +- 15 files changed, 1232 insertions(+), 15 deletions(-) create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/DefaultURLParser.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/PathResolver.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/PercentEncoder.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/URL.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/URLBuilder.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/URLParser.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/URLPart.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidHexException.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidURLReferenceException.java create mode 100644 lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/MalformedURLException.java diff --git a/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddress.java b/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddress.java index ab65de947..2d7a9a969 100644 --- a/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddress.java +++ b/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddress.java @@ -1,5 +1,6 @@ package lc.esp.sdk; +import com.anthonynsimon.url.URL; import lc.mecha.log.MechaLogger; import lc.mecha.log.MechaLoggerFactory; import lc.mecha.util.StringAccumulatorV2; @@ -53,13 +54,33 @@ public class ESPAddress { } } - public ESPAddress(String org, String domain, String service, String name, ESPAddressClass serviceClass, ESPMessageClass destClass) { + public ESPAddress(String org, String domain, String service, String name, + ESPAddressClass serviceClass, ESPMessageClass destClass) { this.org = org; this.domain = domain; this.service = service; this.name = name; this.addressClass = serviceClass; this.messageClass = destClass; + + } + + /** + * Parses a URL in string format. + *

+ * queue://name + * topic://name + */ + public ESPAddress(String address) throws Exception { + URL addrUrl = URL.parse(address); + + String[] host = addrUrl.getHostname().split("\\."); + this.addressClass = ESPAddressClass.val(addrUrl.getScheme()); + this.org = host[0]; + this.domain = host[1]; + this.service = host[2]; + this.name = host[3]; + this.messageClass = ESPMessageClass.val(host[4]); } public String getDomain() { diff --git a/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddressClass.java b/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddressClass.java index 5889d1451..9edc63a7f 100644 --- a/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddressClass.java +++ b/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPAddressClass.java @@ -1,5 +1,7 @@ package lc.esp.sdk; +import java.util.Locale; + /** * This enum defines the semantics for an {@link ESPAddress}. The two options available are topic or queue whose * nomenclature lines up with JMS and other popular Message Queue implementations. @@ -8,5 +10,15 @@ package lc.esp.sdk; * @since mk18 (GIPSY DANGER) */ public enum ESPAddressClass { - TOPIC, QUEUE + TOPIC, QUEUE; + + public static ESPAddressClass val(String str) { + switch (str.toLowerCase(Locale.ROOT)) { + case "topic": + return TOPIC; + case "queue": + return QUEUE; + } + throw new IllegalStateException("Unknown class: " + str); + } } diff --git a/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPClient.java b/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPClient.java index c27dd5506..26630342a 100644 --- a/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPClient.java +++ b/lc-esp-sdk/src/main/java/lc/esp/sdk/ESPClient.java @@ -56,11 +56,8 @@ public class ESPClient implements AutoCloseable { public ESPClient start() throws Exception { new Thread(zero).start(); ZeroActivationIndex zai; - while (true) { - zai = zero.getZai(); - if (zai != null) break; - Thread.sleep(1000); - } + + zai = zero.getZai(); ZeroServiceConfig espCfg = zai.readConfig(SVC_ESP); diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/DefaultURLParser.java b/lc-mecha/src/main/java/com/anthonynsimon/url/DefaultURLParser.java new file mode 100644 index 000000000..2f53bfe30 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/DefaultURLParser.java @@ -0,0 +1,230 @@ +package com.anthonynsimon.url; + +import com.anthonynsimon.url.exceptions.MalformedURLException; + +/** + * A default URL parser implementation. + */ +final class DefaultURLParser implements URLParser { + + /** + * Returns a the URL with the new values after parsing the provided URL string. + */ + public URL parse(String rawUrl) throws MalformedURLException { + if (rawUrl == null || rawUrl.isEmpty()) { + throw new MalformedURLException("raw url string is empty"); + } + + URLBuilder builder = new URLBuilder(); + String remaining = rawUrl; + + int index = remaining.lastIndexOf('#'); + if (index >= 0) { + String frag = remaining.substring(index + 1, remaining.length()); + builder.setFragment(frag.isEmpty() ? null : frag); + remaining = remaining.substring(0, index); + } + + if (remaining.isEmpty()) { + return builder.build(); + } + + if ("*".equals(remaining)) { + builder.setPath("*"); + return builder.build(); + } + + index = remaining.indexOf('?'); + if (index > 0) { + String query = remaining.substring(index + 1, remaining.length()); + if (query.isEmpty()) { + builder.setQuery("?"); + } else { + builder.setQuery(query); + } + remaining = remaining.substring(0, index); + } + + PartialParseResult parsedScheme = parseScheme(remaining); + String scheme = parsedScheme.result; + boolean hasScheme = scheme != null && !scheme.isEmpty(); + builder.setScheme(scheme); + remaining = parsedScheme.remaining; + + if (hasScheme && !remaining.startsWith("/")) { + builder.setOpaque(remaining); + return builder.build(); + } + if ((hasScheme || !remaining.startsWith("///")) && remaining.startsWith("//")) { + remaining = remaining.substring(2, remaining.length()); + + String authority = remaining; + int i = remaining.indexOf('/'); + if (i >= 0) { + authority = remaining.substring(0, i); + remaining = remaining.substring(i, remaining.length()); + } else { + remaining = ""; + } + + if (!authority.isEmpty()) { + UserInfoResult userInfoResult = parseUserInfo(authority); + builder.setUsername(userInfoResult.user); + builder.setPassword(userInfoResult.password); + authority = userInfoResult.remaining; + } + + PartialParseResult hostResult = parseHost(authority); + builder.setHost(hostResult.result); + } + + if (!remaining.isEmpty()) { + builder.setPath(PercentEncoder.decode(remaining)); + builder.setRawPath(remaining); + } + + return builder.build(); + } + + /** + * Parses the scheme from the provided string. + *

+ * * @throws MalformedURLException if there was a problem parsing the input string. + */ + private PartialParseResult parseScheme(String remaining) throws MalformedURLException { + int indexColon = remaining.indexOf(':'); + if (indexColon == 0) { + throw new MalformedURLException("missing scheme"); + } + if (indexColon < 0) { + return new PartialParseResult("", remaining); + } + + // if first char is special then its not a scheme + char first = remaining.charAt(0); + if ('0' <= first && first <= '9' || first == '+' || first == '-' || first == '.') { + return new PartialParseResult("", remaining); + } + + String scheme = remaining.substring(0, indexColon).toLowerCase(); + String rest = remaining.substring(indexColon + 1, remaining.length()); + + return new PartialParseResult(scheme, rest); + } + + /** + * Parses the authority (user:password@host:port) from the provided string. + * + * @throws MalformedURLException if there was a problem parsing the input string. + */ + private UserInfoResult parseUserInfo(String str) throws MalformedURLException { + int i = str.lastIndexOf('@'); + String username = null; + String password = null; + String rest = str; + if (i >= 0) { + String credentials = str.substring(0, i); + if (credentials.indexOf(':') >= 0) { + String[] parts = credentials.split(":", 2); + username = PercentEncoder.decode(parts[0]); + password = PercentEncoder.decode(parts[1]); + } else { + username = PercentEncoder.decode(credentials); + } + rest = str.substring(i + 1, str.length()); + } + + return new UserInfoResult(username, password, rest); + } + + /** + * Parses the host from the provided string. The port is considered part of the host and + * will be checked to ensure that it's a numeric value. + * + * @throws MalformedURLException if there was a problem parsing the input string. + */ + private PartialParseResult parseHost(String str) throws MalformedURLException { + if (str.length() == 0) { + return new PartialParseResult("", ""); + } + if (str.charAt(0) == '[') { + int i = str.lastIndexOf(']'); + if (i < 0) { + throw new MalformedURLException("IPv6 detected, but missing closing ']' token"); + } + String portPart = str.substring(i + 1, str.length()); + if (!isPortValid(portPart)) { + throw new MalformedURLException("invalid port"); + } + } else { + if (str.indexOf(':') != -1) { + String[] parts = str.split(":", -1); + if (parts.length > 2) { + throw new MalformedURLException("invalid host in: " + str); + } + if (parts.length == 2) { + try { + Integer.valueOf(parts[1]); + } catch (NumberFormatException e) { + throw new MalformedURLException("invalid port"); + } + } + } + } + return new PartialParseResult(PercentEncoder.decode(str.toLowerCase()), ""); + } + + /** + * Returns true if the provided port string contains a valid port number. + * Note that an empty string is a valid port number since it's optional. + *

+ * For example: + *

+ * '' => TRUE + * null => TRUE + * ':8080' => TRUE + * ':ab80' => FALSE + * ':abc' => FALSE + */ + protected boolean isPortValid(String portStr) { + if (portStr == null || portStr.isEmpty()) { + return true; + } + int i = portStr.indexOf(':'); + // Port format must be ':8080' + if (i != 0) { + return false; + } + String segment = portStr.substring(i + 1, portStr.length()); + try { + Integer.valueOf(segment); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + private class PartialParseResult { + public final String result; + public final String remaining; + + + public PartialParseResult(String result, String remaining) { + this.result = result; + this.remaining = remaining; + } + } + + private class UserInfoResult { + public final String user; + public final String password; + public final String remaining; + + + public UserInfoResult(String user, String password, String remaining) { + this.user = user; + this.password = password; + this.remaining = remaining; + } + } +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/PathResolver.java b/lc-mecha/src/main/java/com/anthonynsimon/url/PathResolver.java new file mode 100644 index 000000000..69f4f80f0 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/PathResolver.java @@ -0,0 +1,109 @@ +package com.anthonynsimon.url; + +import java.util.ArrayList; +import java.util.List; + +/** + * PathResolver is a utility class that resolves a reference path against a base path. + */ +final class PathResolver { + + /** + * Disallow instantiation of class. + */ + private PathResolver() { + } + + /** + * Returns a resolved path. + *

+ * For example: + *

+ * resolve("/some/path", "..") == "/some" + * resolve("/some/path", ".") == "/some/" + * resolve("/some/path", "./here") == "/some/here" + * resolve("/some/path", "../here") == "/here" + */ + public static String resolve(String base, String ref) { + String merged = merge(base, ref); + if (merged == null || merged.isEmpty()) { + return ""; + } + String[] parts = merged.split("/", -1); + return resolve(parts); + } + + /** + * Returns the two path strings merged into one. + *

+ * For example: + * + * merge("/some/path", "./../hello") == "/some/./../hello" + * merge("/some/path/", "./../hello") == "/some/path/./../hello" + * merge("/some/path/", "") == "/some/path/" + * merge("", "/some/other/path") == "/some/other/path" + */ + private static String merge(String base, String ref) { + String merged; + + if (ref == null || ref.isEmpty()) { + merged = base; + } else if (ref.charAt(0) != '/' && base != null && !base.isEmpty()) { + int i = base.lastIndexOf("/"); + merged = base.substring(0, i + 1) + ref; + } else { + merged = ref; + } + + if (merged == null || merged.isEmpty()) { + return ""; + } + + return merged; + } + + /** + * Returns the resolved path parts. + *

+ * Example: + *

+ * resolve(String[]{"some", "path", "..", "hello"}) == "/some/hello" + */ + private static String resolve(String[] parts) { + if (parts.length == 0) { + return ""; + } + + List result = new ArrayList<>(); + + for (int i = 0; i < parts.length; i++) { + switch (parts[i]) { + case "": + case ".": + // Ignore + break; + case "..": + if (result.size() > 0) { + result.remove(result.size() - 1); + } + break; + default: + result.add(parts[i]); + break; + } + } + + // Get last element, if it was '.' or '..' we need + // to end in a slash. + switch (parts[parts.length - 1]) { + case ".": + case "..": + // Add an empty last string, it will be turned into + // a slash when joined together. + result.add(""); + break; + } + + return "/" + String.join("/", result); + } +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/PercentEncoder.java b/lc-mecha/src/main/java/com/anthonynsimon/url/PercentEncoder.java new file mode 100644 index 000000000..da08f4ba4 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/PercentEncoder.java @@ -0,0 +1,235 @@ +package com.anthonynsimon.url; + +import com.anthonynsimon.url.exceptions.InvalidHexException; +import com.anthonynsimon.url.exceptions.MalformedURLException; + +import java.nio.charset.StandardCharsets; + +/** + * PercentEncoder handles the escaping and unescaping of characters in URLs. + * It escapes character based on the context (part of the URL) that is being dealt with. + *

+ * Supports UTF-8 escaping and unescaping. + */ +final class PercentEncoder { + + /** + * Reserved characters, allowed in certain parts of the URL. Must be escaped in most cases. + */ + private static final char[] reservedChars = {'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"'}; + /** + * Unreserved characters do not need to be escaped. + */ + private static final char[] unreservedChars = {'-', '_', '.', '~'}; + /** + * Byte masks to aid in the decoding of UTF-8 byte arrays. + */ + private static final short[] utf8Masks = new short[]{0b00000000, 0b11000000, 0b11100000, 0b11110000}; + + /** + * Character set for Hex Strings + */ + private static final String hexSet = "0123456789ABCDEF"; + + /** + * Disallow instantiation of class. + */ + private PercentEncoder() { + } + + /** + * Returns true if escaping is required based on the character and encode zone provided. + */ + private static boolean shouldEscapeChar(char c, URLPart zone) { + if ('A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9') { + return false; + } + + if (zone == URLPart.HOST || zone == URLPart.PATH) { + if (c == '%') { + return true; + } + for (char reserved : reservedChars) { + if (reserved == c) { + return false; + } + } + } + + for (char unreserved : unreservedChars) { + if (unreserved == c) { + return false; + } + } + + for (char reserved : new char[]{'$', '&', '+', ',', '/', ':', ';', '=', '?', '@'}) { + if (reserved == c) { + switch (zone) { + case PATH: + return c == '?'; + case CREDENTIALS: + return c == '@' || c == '/' || c == '?' || c == ':'; + case QUERY: + return true; + case FRAGMENT: + return false; + default: + return true; + } + } + } + + return true; + } + + private static boolean needsEscaping(String str, URLPart zone) { + char[] chars = str.toCharArray(); + for (char c : chars) { + if (shouldEscapeChar(c, zone)) { + return true; + } + } + return false; + } + + private static boolean needsUnescaping(String str) { + return (str.indexOf('%') >= 0); + } + + /** + * Returns a percent-escaped string. Each character will be evaluated in case it needs to be escaped + * based on the provided EncodeZone. + */ + public static String encode(String str, URLPart zone) { + // The string might not need escaping at all, check first. + if (!needsEscaping(str, zone)) { + return str; + } + + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + + int i = 0; + String result = ""; + while (i < bytes.length) { + int readBytes = 0; + for (short mask : utf8Masks) { + if ((bytes[i] & mask) == mask) { + readBytes++; + } else { + break; + } + } + for (int j = 0; j < readBytes; j++) { + char c = (char) bytes[i]; + if (shouldEscapeChar(c, zone)) { + result += "%" + hexSet.charAt((bytes[i] & 0xFF) >> 4) + hexSet.charAt((bytes[i] & 0xFF) & 15); + } else { + result += c; + } + i++; + } + } + + return result; + } + + /** + * Returns an unescaped string. + * + * @throws MalformedURLException if an invalid escape sequence is found. + */ + public static String decode(String str) throws MalformedURLException { + // The string might not need unescaping at all, check first. + if (!needsUnescaping(str)) { + return str; + } + + char[] chars = str.toCharArray(); + String result = ""; + int len = str.length(); + int i = 0; + while (i < chars.length) { + char c = chars[i]; + if (c != '%') { + result += c; + i++; + } else { + if (i + 2 >= len) { + throw new MalformedURLException("invalid escape sequence"); + } + byte code; + try { + code = unhex(str.substring(i + 1, i + 3).toCharArray()); + } catch (InvalidHexException e) { + throw new MalformedURLException(e.getMessage()); + } + int readBytes = 0; + for (short mask : utf8Masks) { + if ((code & mask) == mask) { + readBytes++; + } else { + break; + } + } + byte[] buffer = new byte[readBytes]; + for (int j = 0; j < readBytes; j++) { + if (str.charAt(i) != '%') { + byte[] currentBuffer = new byte[j]; + for (int h = 0; h < j; h++) { + currentBuffer[h] = buffer[h]; + } + buffer = currentBuffer; + break; + } + if (i + 3 > len) { + buffer = "\uFFFD".getBytes(); + break; + } + try { + buffer[j] = unhex(str.substring(i + 1, i + 3).toCharArray()); + } catch (InvalidHexException e) { + throw new MalformedURLException(e.getMessage()); + } + i += 3; + } + result += new String(buffer); + } + } + return result; + } + + /** + * Returns a byte representation of a parsed array of hex chars. + * + * @throws InvalidHexException if the provided array of hex characters is invalid. + */ + private static byte unhex(char[] hex) throws InvalidHexException { + int result = 0; + for (int i = 0; i < hex.length; i++) { + char c = hex[hex.length - i - 1]; + int index = -1; + if ('0' <= c && c <= '9') { + index = c - '0'; + } else if ('a' <= c && c <= 'f') { + index = c - 'a' + 10; + } else if ('A' <= c && c <= 'F') { + index = c - 'A' + 10; + } + if (index < 0 || index >= 16) { + throw new InvalidHexException("not a valid hex char: " + c); + } + result += index * pow(16, i); + } + return (byte) result; + } + + private static int pow(int base, int exp) { + int result = 1; + int expRemaining = exp; + while (expRemaining > 0) { + result *= base; + expRemaining--; + } + return result; + } +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/URL.java b/lc-mecha/src/main/java/com/anthonynsimon/url/URL.java new file mode 100644 index 000000000..8c2f25244 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/URL.java @@ -0,0 +1,450 @@ +package com.anthonynsimon.url; + +import com.anthonynsimon.url.exceptions.InvalidURLReferenceException; +import com.anthonynsimon.url.exceptions.MalformedURLException; + +import java.io.Serializable; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * URL is a reference to a web resource. This class implements functionality for parsing and + * manipulating the various parts that make up a URL. + *

+ * Once parsed it is of the form: + *

+ * scheme:[//[user:password@]host[:port]][/]path[?query][#fragment] + */ +public final class URL implements Serializable { + + /** + * Unique ID for serialization purposes. + */ + private static final long serialVersionUID = 80443L; + + /** + * URLParser to be used to parse the URL string into the URL object. + * Do not serialize. + */ + private transient final static URLParser URL_PARSER = new DefaultURLParser(); + + private final String scheme; + private final String username; + private final String password; + private final String host; + private final String hostname; + private final Integer port; + private final String path; + private final String rawPath; + private final String query; + private final String fragment; + private final String opaque; + + /** + * Cached parsed query string key-value pairs. + * Do not serialize. + */ + private transient Map> parsedQueryPairs; + + /** + * Cached string representation of the URL. + * Do not serialize. + */ + private transient String stringRepresentation; + + /** + * Protect instantiation of class. Use public parse method instead to construct URLs. + * Builder is for protected use only. + */ + URL(String scheme, String username, String password, String host, String path, String query, String fragment, String opaque) { + this.scheme = mapToNullIfEmpty(scheme); + this.username = mapToNullIfEmpty(username); + this.password = mapToNullIfEmpty(password); + this.host = mapToNullIfEmpty(host); + this.hostname = extractHostname(host); + this.port = extractPort(host); + this.path = mapToNullIfEmpty(path); + this.rawPath = null; + this.query = mapToNullIfEmpty(query); + this.fragment = mapToNullIfEmpty(fragment); + this.opaque = mapToNullIfEmpty(opaque); + } + + URL(String scheme, String username, String password, String host, String path, String rawPath, String query, String fragment, String opaque) { + this.scheme = mapToNullIfEmpty(scheme); + this.username = mapToNullIfEmpty(username); + this.password = mapToNullIfEmpty(password); + this.host = mapToNullIfEmpty(host); + this.hostname = extractHostname(host); + this.port = extractPort(host); + this.path = mapToNullIfEmpty(path); + this.rawPath = mapToNullIfEmpty(rawPath); + this.query = mapToNullIfEmpty(query); + this.fragment = mapToNullIfEmpty(fragment); + this.opaque = mapToNullIfEmpty(opaque); + } + + URL(String scheme, String username, String password, String hostname, Integer port, String path, String rawPath, String query, String fragment, String opaque) { + this.scheme = mapToNullIfEmpty(scheme); + this.username = mapToNullIfEmpty(username); + this.password = mapToNullIfEmpty(password); + this.host = mergeHostPortIfSet(hostname, port); + this.hostname = mapToNullIfEmpty(hostname); + this.port = port; + this.path = mapToNullIfEmpty(path); + this.rawPath = mapToNullIfEmpty(rawPath); + this.query = mapToNullIfEmpty(query); + this.fragment = mapToNullIfEmpty(fragment); + this.opaque = mapToNullIfEmpty(opaque); + } + + + /** + * Returns a new URL object after parsing the provided URL string. + */ + public static URL parse(String url) throws MalformedURLException { + return URL_PARSER.parse(url); + } + + /** + * Returns the scheme ('http' or 'file' or 'ftp' etc...) of the URL if it exists. + */ + public String getScheme() { + return scheme; + } + + /** + * Returns the username part of the userinfo if it exists. + */ + public String getUsername() { + return username; + } + + /** + * Returns the password part of the userinfo if it exists. + */ + public String getPassword() { + return password; + } + + /** + * Returns the host ('www.example.com' or '192.168.0.1:8080' or '[fde2:d7de:302::]') of the URL if it exists. + */ + public String getHost() { + return host; + } + + /** + * Returns the hostname part of the host ('www.example.com' or '192.168.0.1' or '[fde2:d7de:302::]') if it exists. + */ + public String getHostname() { + return hostname; + } + + /** + * Returns the port part of the host (i.e. 80 or 443 or 3000) if it exists. + */ + public Integer getPort() { + return port; + } + + /** + * Returns the unescaped path ('/path/to/the;/file.html') of the URL if it exists. + */ + public String getPath() { + return path; + } + + + /** + * Returns the raw path ('/path/to/the%3B/file.html') of the URL if it exists. + */ + public String getRawPath() { + return rawPath; + } + + /** + * Returns the query ('?q=foo{@literal &}bar') of the URL if it exists. + */ + public String getQuery() { + return query; + } + + /** + * Returns the fragment ('#foo{@literal &}bar') of the URL if it exists. + */ + public String getFragment() { + return fragment; + } + + /** + * Returns the opaque part of the URL if it exists. + */ + public String getOpaque() { + return opaque; + } + + /** + * Returns a java.net.URL object from the parsed url. + * + * @throws java.net.MalformedURLException if something went wrong while created the new object. + */ + public java.net.URL toJavaURL() throws java.net.MalformedURLException { + return new java.net.URL(toString()); + } + + /** + * Returns a java.net.URI object from the parsed url. + * + * @throws java.net.URISyntaxException if something went wrong while created the new object. + */ + public java.net.URI toJavaURI() throws URISyntaxException { + return new URI(toString()); + } + + + /** + * Returns a map of key-value pairs from the parsed query string. + */ + public Map> getQueryPairs() { + if (parsedQueryPairs != null) { + return parsedQueryPairs; + } + parsedQueryPairs = new HashMap<>(); + + if (!nullOrEmpty(query) && !query.equals("?")) { + String[] pairs = query.split("&"); + for (String pair : pairs) { + String[] parts = pair.split("="); + if (parts.length > 0 && !parts[0].isEmpty()) { + Collection existing = parsedQueryPairs.getOrDefault(parts[0], new ArrayList<>()); + if (parts.length == 2) { + existing.add(parts[1]); + } + parsedQueryPairs.put(parts[0], existing); + } + } + } + + return parsedQueryPairs; + } + + /** + * Returns true if the two Objects are instances of URL and their string representations match. + */ + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (!(other instanceof URL)) { + return false; + } + return toString().equals(other.toString()); + } + + /** + * Returns a string representation of the all parts of the URL that are not null. + */ + @Override + public String toString() { + if (stringRepresentation != null) { + return stringRepresentation; + } + + boolean hasScheme = !nullOrEmpty(scheme); + StringBuffer sb = new StringBuffer(); + if (hasScheme) { + sb.append(scheme); + sb.append(":"); + } + if (!nullOrEmpty(opaque)) { + sb.append(opaque); + } else { + if (hasScheme || !nullOrEmpty(host)) { + if (hasScheme) { + sb.append("//"); + } + if (!nullOrEmpty(username)) { + sb.append(PercentEncoder.encode(username, URLPart.CREDENTIALS)); + if (!nullOrEmpty(password)) { + sb.append(":"); + sb.append(PercentEncoder.encode(password, URLPart.CREDENTIALS)); + } + sb.append("@"); + } + if (!nullOrEmpty(host)) { + sb.append(PercentEncoder.encode(host, URLPart.HOST)); + } + } + if (!nullOrEmpty(rawPath)) { + sb.append(rawPath); + } else if (!nullOrEmpty(path)) { + if (path.indexOf('/') != 0 && !"*".equals(path)) { + sb.append("/"); + } + sb.append(path); + } + } + if (!nullOrEmpty(query)) { + sb.append("?"); + // Only append '?' if that's all there is to the query string + if (!"?".equals(query)) { + sb.append(query); + } + } + if (!nullOrEmpty(fragment)) { + sb.append("#"); + sb.append(fragment); + } + + stringRepresentation = sb.toString(); + + return stringRepresentation; + } + + /** + * Returns the hash code of the URL. + */ + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * Returns true if the parameter string is neither null nor empty (""). + */ + private boolean nullOrEmpty(String str) { + return str == null || str.isEmpty(); + } + + /** + * Returns true if URL is opaque. + */ + public boolean isOpaque() { + return !nullOrEmpty(opaque); + } + + /** + * Returns true if URL is absolute. + */ + public boolean isAbsolute() { + return !nullOrEmpty(scheme); + } + + public URL resolveReference(String ref) throws MalformedURLException, InvalidURLReferenceException { + URL url = URL_PARSER.parse(ref); + return resolveReference(url); + } + + /** + * Returns the resolved reference URL using the instance URL as a base. + *

+ * If the reference URL is absolute, then it simply creates a new URL that is identical to it + * and returns it. If the reference and the base URLs are identical, a new instance of the reference is returned. + * + * @throws InvalidURLReferenceException if the provided ref URL is invalid or if the base URL is not absolute. + */ + public URL resolveReference(URL ref) throws InvalidURLReferenceException { + if (!isAbsolute()) { + throw new InvalidURLReferenceException("base url is not absolute"); + } + if (ref == null) { + throw new InvalidURLReferenceException("reference url is null"); + } + + URLBuilder builder = new URLBuilder() + .setScheme(ref.getScheme()) + .setUsername(ref.getUsername()) + .setPassword(ref.getPassword()) + .setHost(ref.getHost()) + .setPath(ref.getPath()) + .setQuery(ref.getQuery()) + .setFragment(ref.getFragment()) + .setOpaque(ref.getOpaque()); + + if (!ref.isAbsolute()) { + builder.setScheme(scheme); + } + + if (!nullOrEmpty(ref.scheme) || !nullOrEmpty(ref.host)) { + builder.setPath(PathResolver.resolve(ref.path, "")); + return builder.build(); + } + + if (ref.isOpaque() || isOpaque()) { + return builder.build(); + } + + return builder + .setHost(host) + .setUsername(username) + .setPassword(password) + .setPath(PathResolver.resolve(path, ref.path)) + .build(); + } + + + private static String mapToNullIfEmpty(String str) { + return str != null && !str.isEmpty() ? str : null; + } + + /** + * Returns the full host (hostname:port), maps to null if none are set. + */ + private static String mergeHostPortIfSet(String hostname, Integer port) { + StringBuilder sb = new StringBuilder(); + boolean exists = false; + if (hostname != null) { + sb.append(hostname); + exists = true; + } + if (port != null) { + sb.append(":"); + sb.append(port); + exists = true; + } + if (exists) { + return sb.toString(); + } + return null; + } + + /** + * Returns the hostname part of the host ('www.example.com' or '192.168.0.1' or '[fde2:d7de:302::]') if it exists. + */ + private static String extractHostname(String host) { + if (host != null) { + int separator = host.lastIndexOf(":"); + if (separator > -1) { + return host.substring(0, separator); + } + return host; + } + return null; + } + + /** + * Returns the port part of the host (i.e. 8080 or 443 or 3000) if it exists. + */ + private static Integer extractPort(String host) { + if (host != null) { + int separator = host.lastIndexOf(":"); + if (separator > -1) { + String part = host.substring(separator + 1, host.length()); + if (part != null && part != "") { + try { + return Integer.parseInt(part); + } catch (NumberFormatException exception) { + return null; + } + } + } + } + return null; + } +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/URLBuilder.java b/lc-mecha/src/main/java/com/anthonynsimon/url/URLBuilder.java new file mode 100644 index 000000000..7babb63c1 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/URLBuilder.java @@ -0,0 +1,65 @@ +package com.anthonynsimon.url; + +/** + * URLBuilder is a helper class for the construction of a URL object. + */ +final class URLBuilder { + private String scheme; + private String username; + private String password; + private String host; + private String path; + private String rawPath; + private String query; + private String fragment; + private String opaque; + + public URL build() { + return new URL(scheme, username, password, host, path, rawPath, query, fragment, opaque); + } + + public URLBuilder setScheme(String scheme) { + this.scheme = scheme; + return this; + } + + public URLBuilder setUsername(String username) { + this.username = username; + return this; + } + + public URLBuilder setPassword(String password) { + this.password = password; + return this; + } + + public URLBuilder setHost(String host) { + this.host = host; + return this; + } + + public URLBuilder setPath(String path) { + this.path = path; + return this; + } + + public URLBuilder setRawPath(String rawPath) { + this.rawPath = rawPath; + return this; + } + + public URLBuilder setQuery(String query) { + this.query = query; + return this; + } + + public URLBuilder setFragment(String fragment) { + this.fragment = fragment; + return this; + } + + public URLBuilder setOpaque(String opaque) { + this.opaque = opaque; + return this; + } +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/URLParser.java b/lc-mecha/src/main/java/com/anthonynsimon/url/URLParser.java new file mode 100644 index 000000000..f2ca38128 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/URLParser.java @@ -0,0 +1,13 @@ +package com.anthonynsimon.url; + +import com.anthonynsimon.url.exceptions.MalformedURLException; + +/** + * URLParser handles the parsing of a URL string into a URL object. + */ +interface URLParser { + /** + * Returns a the URL with the new values after parsing the provided URL string. + */ + URL parse(String url) throws MalformedURLException; +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/URLPart.java b/lc-mecha/src/main/java/com/anthonynsimon/url/URLPart.java new file mode 100644 index 000000000..dfcb83d94 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/URLPart.java @@ -0,0 +1,13 @@ +package com.anthonynsimon.url; + +/** + * URLPart is used to distinguish between the parts of the url when encoding/decoding. + */ +enum URLPart { + CREDENTIALS, + HOST, + PATH, + QUERY, + FRAGMENT, + ENCODE_ZONE, +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidHexException.java b/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidHexException.java new file mode 100644 index 000000000..84e1667af --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidHexException.java @@ -0,0 +1,15 @@ +package com.anthonynsimon.url.exceptions; + +/** + * InvalidHexException is thrown when parsing a Hexadecimal was not + * possible due to bad input. + */ +public class InvalidHexException extends Exception { + public InvalidHexException() { + super(); + } + + public InvalidHexException(String message) { + super(message); + } +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidURLReferenceException.java b/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidURLReferenceException.java new file mode 100644 index 000000000..402d83232 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/InvalidURLReferenceException.java @@ -0,0 +1,15 @@ +package com.anthonynsimon.url.exceptions; + +/** + * InvalidURLReferenceException is thrown when attempting to resolve a relative URL against an + * absolute URL and something went wrong. + */ +public class InvalidURLReferenceException extends Exception { + public InvalidURLReferenceException() { + super(); + } + + public InvalidURLReferenceException(String message) { + super(message); + } +} diff --git a/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/MalformedURLException.java b/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/MalformedURLException.java new file mode 100644 index 000000000..26d724e53 --- /dev/null +++ b/lc-mecha/src/main/java/com/anthonynsimon/url/exceptions/MalformedURLException.java @@ -0,0 +1,15 @@ +package com.anthonynsimon.url.exceptions; + +/** + * MalformedURLException is thrown when parsing a URL or part of it and it was not + * possible to complete the operation due to bad input. + */ +public class MalformedURLException extends Exception { + public MalformedURLException() { + super(); + } + + public MalformedURLException(String message) { + super(message); + } +} diff --git a/lc-telemetry-historian-service/src/main/java/lc/telemetry/historian/service/HistorianService.java b/lc-telemetry-historian-service/src/main/java/lc/telemetry/historian/service/HistorianService.java index a88f2f485..122b37e1c 100644 --- a/lc-telemetry-historian-service/src/main/java/lc/telemetry/historian/service/HistorianService.java +++ b/lc-telemetry-historian-service/src/main/java/lc/telemetry/historian/service/HistorianService.java @@ -11,6 +11,8 @@ import lc.mecha.util.BasicallyDangerous; import lc.mecha.util.StringAccumulatorV2; import lc.mecha.util.UniversalJob; import lc.mecha.util.VelocityWatch; +import lc.zero.sdk.ZeroClient; +import lc.zero.sdk.ZeroServiceConfig; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.BasicHttpEntity; @@ -35,6 +37,7 @@ import java.util.concurrent.ThreadPoolExecutor; * @since mk18 (GIPSY DANGER) */ public class HistorianService extends BasicallyDangerous { + public static final String ZERO_SERVICE = "historian"; public static final String TSDB_KEY_METRIC = "metric"; public static final String TSDB_KEY_TIMESTAMP = "timestamp"; public static final String TSDB_KEY_VALUE = "value"; @@ -58,14 +61,30 @@ public class HistorianService extends BasicallyDangerous { @Override public void runDangerously() throws Exception { - ESPAddress telemDest = new ESPAddress("lc", "global", - "env", "mon", ESPAddressClass.TOPIC, - ESPMessageClass.TELEMETRY); - logger.info("Generated MQTT Address: {}", telemDest.toMQTTAddress()); try (ESPClient esp = new ESPClient()) { esp.start(); + + ZeroClient zero = esp.getZero(); + ZeroServiceConfig cfg = zero.getZai().readConfig(ZERO_SERVICE); + if (cfg == null) { + logger.error("No ZERO service configuration provided."); + System.exit(UniversalJob.RET_BADENV); + } + logger.info("Found service configuration: {}", cfg); + + String addrUrl = cfg.getCfg().getString("address"); + ESPAddress telemDest = new ESPAddress(addrUrl); + logger.info("Found address: {}", telemDest); + + /* + ESPAddress telemDest = new ESPAddress("lc", "global", + "env", "mon", ESPAddressClass.TOPIC, + ESPMessageClass.TELEMETRY); + + */ + try (ESPSession session = esp.createSession()) { try (ESPConsumer consumer = session.createConsumer(telemDest)) { //noinspection InfiniteLoopStatement diff --git a/lc-zero-sdk/src/main/java/lc/zero/sdk/ZeroClient.java b/lc-zero-sdk/src/main/java/lc/zero/sdk/ZeroClient.java index 1890a2796..17e09c149 100644 --- a/lc-zero-sdk/src/main/java/lc/zero/sdk/ZeroClient.java +++ b/lc-zero-sdk/src/main/java/lc/zero/sdk/ZeroClient.java @@ -51,6 +51,8 @@ public class ZeroClient implements Runnable { logger.error(ENV_LC_ZAI + " defined, but an error occurred: " + e); System.exit(UniversalJob.RET_BADENV); } + } else { + logger.info(ENV_LC_ZAI + " unset. Will perform network discovery."); } try { @@ -74,7 +76,15 @@ public class ZeroClient implements Runnable { } public ZeroActivationIndex getZai() { - return zai; + while (true) { + if (zai != null) return zai; + logger.info("Waiting for ZAI discovery..."); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } private void processBeacon(Beacon b) throws IOException { @@ -100,6 +110,4 @@ public class ZeroClient implements Runnable { zai = new ZeroActivationIndex(httpClient, json); logger.info("Read ZAI: {}", zai); } - - -} +} \ No newline at end of file -- GitLab